From 5f618ba516fc28cfced0ed838b20fbd27055d33a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 08:56:23 -0700 Subject: [PATCH 1/5] core tests: submit turns with permission profiles --- codex-rs/core/tests/common/test_codex.rs | 74 +++++++++++-- codex-rs/core/tests/suite/openai_file_mcp.rs | 6 +- codex-rs/core/tests/suite/search_tool.rs | 34 +++--- .../core/tests/suite/shell_serialization.rs | 66 ++++++------ codex-rs/core/tests/suite/tool_suggest.rs | 6 +- codex-rs/core/tests/suite/truncation.rs | 44 +++++--- codex-rs/core/tests/suite/web_search.rs | 24 ++--- codex-rs/tui/src/app_command.rs | 17 ++- codex-rs/tui/src/app_server_session.rs | 65 ++++++++--- codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/permission_compat.rs | 102 ++++++++++++++++++ 11 files changed, 322 insertions(+), 117 deletions(-) create mode 100644 codex-rs/tui/src/permission_compat.rs diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index d77d55956a..fd92d0fde1 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -28,6 +28,7 @@ use codex_model_provider_info::built_in_model_providers; use codex_models_manager::bundled_models_response; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -591,10 +592,19 @@ impl TestCodex { } pub async fn submit_turn(&self, prompt: &str) -> Result<()> { - self.submit_turn_with_policies( + self.submit_turn_with_permission_profile(prompt, PermissionProfile::Disabled) + .await + } + + pub async fn submit_turn_with_permission_profile( + &self, + prompt: &str, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.submit_turn_with_approval_and_permission_profile( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + permission_profile, ) .await } @@ -613,10 +623,10 @@ impl TestCodex { prompt: &str, service_tier: Option, ) -> Result<()> { - self.submit_turn_with_context( + self.submit_turn_with_permission_profile_context( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(service_tier), /*environments*/ None, ) @@ -633,6 +643,23 @@ impl TestCodex { prompt, approval_policy, sandbox_policy, + /*permission_profile*/ None, + /*service_tier*/ None, + /*environments*/ None, + ) + .await + } + + pub async fn submit_turn_with_approval_and_permission_profile( + &self, + prompt: &str, + approval_policy: AskForApproval, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.submit_turn_with_permission_profile_context( + prompt, + approval_policy, + permission_profile, /*service_tier*/ None, /*environments*/ None, ) @@ -644,21 +671,44 @@ impl TestCodex { prompt: &str, environments: Option>, ) -> Result<()> { - self.submit_turn_with_context( + self.submit_turn_with_permission_profile_context( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, /*service_tier*/ None, environments, ) .await } + async fn submit_turn_with_permission_profile_context( + &self, + prompt: &str, + approval_policy: AskForApproval, + permission_profile: PermissionProfile, + service_tier: Option>, + environments: Option>, + ) -> Result<()> { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(self.config.cwd.as_path()) + .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + self.submit_turn_with_context( + prompt, + approval_policy, + sandbox_policy, + Some(permission_profile), + service_tier, + environments, + ) + .await + } + async fn submit_turn_with_context( &self, prompt: &str, approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, + permission_profile: Option, service_tier: Option>, environments: Option>, ) -> Result<()> { @@ -675,7 +725,7 @@ impl TestCodex { approval_policy, approvals_reviewer: None, sandbox_policy, - permission_profile: None, + permission_profile, model: session_model, effort: None, summary: None, @@ -835,6 +885,16 @@ impl TestCodexHarness { .await } + pub async fn submit_with_permission_profile( + &self, + prompt: &str, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.test + .submit_turn_with_permission_profile(prompt, permission_profile) + .await + } + pub async fn request_bodies(&self) -> Vec { let path_matcher = path_regex(".*/responses$"); self.server diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index 3bfa264d79..ac49b5334b 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -8,8 +8,8 @@ use anyhow::Result; use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::DOCUMENT_EXTRACT_TEXT_RESOURCE_URI; use core_test_support::responses::ev_assistant_message; @@ -169,10 +169,10 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res let test = builder.build(&server).await?; tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Extract the report text with the app tool.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 1794f2fd3f..6870ad203c 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -12,11 +12,11 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpInvocation; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI; @@ -157,10 +157,10 @@ async fn search_tool_enabled_by_default_adds_tool_search() -> Result<()> { let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -221,10 +221,10 @@ async fn always_defer_feature_hides_small_app_tool_sets() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -265,10 +265,10 @@ async fn tool_search_disabled_exposes_apps_tools_directly() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -311,10 +311,10 @@ async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> { .with_config(move |config| configure_apps(config, apps_server.chatgpt_base_url.as_str())); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -347,10 +347,10 @@ async fn search_tool_adds_discovery_instructions_to_tool_description() -> Result let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -389,10 +389,10 @@ async fn search_tool_hides_apps_tools_without_search() -> Result<()> { let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "hello tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -425,10 +425,10 @@ async fn explicit_app_mentions_expose_apps_tools_without_search() -> Result<()> let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Use [$calendar](app://calendar) and then call tools.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -957,10 +957,10 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Find the rmcp echo and image tools.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 446c5ea08f..ed474114ff 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -2,7 +2,7 @@ #![allow(clippy::expect_used)] use anyhow::Result; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -136,9 +136,9 @@ async fn shell_output_stays_json_without_freeform_apply_patch( let responses = shell_responses(call_id, vec!["/bin/echo", "shell json"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the json shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -192,9 +192,9 @@ async fn shell_output_is_structured_with_freeform_apply_patch( let responses = shell_responses(call_id, vec!["/bin/echo", "freeform shell"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the structured shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -249,9 +249,9 @@ async fn shell_output_preserves_fixture_json_without_serialization( )?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "read the fixture JSON with sed", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -317,9 +317,9 @@ async fn shell_output_structures_fixture_with_serialization( )?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "read the fixture JSON with structured output", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -372,9 +372,9 @@ async fn shell_output_for_freeform_tool_records_duration( let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "sleep 0.2"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the structured shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -429,9 +429,9 @@ async fn shell_output_reserializes_truncated_content(output_type: ShellModelOutp let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "seq 1 400"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the truncation shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -495,9 +495,9 @@ async fn apply_patch_custom_tool_output_is_structured( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -537,9 +537,9 @@ async fn apply_patch_custom_tool_call_creates_file( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool to create a file", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -593,9 +593,9 @@ async fn apply_patch_custom_tool_call_updates_existing_file( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool to update a file", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -645,9 +645,9 @@ async fn apply_patch_custom_tool_call_reports_failure_output( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "attempt a failing apply_patch via custom tool", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -688,9 +688,9 @@ async fn apply_patch_function_call_output_is_structured( .await; harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via function-call apply_patch", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -727,9 +727,9 @@ async fn shell_output_is_structured_for_nonzero_exit(output_type: ShellModelOutp let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "exit 42"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the failing shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -778,9 +778,9 @@ async fn shell_command_output_is_freeform() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -830,9 +830,9 @@ async fn shell_command_output_is_not_truncated_under_10k_bytes() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -881,9 +881,9 @@ async fn shell_command_output_is_not_truncated_over_10k_bytes() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -929,9 +929,9 @@ async fn local_shell_call_output_is_structured() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the local shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 30da83e9eb..b5b3752e0c 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -8,8 +8,8 @@ use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; use codex_models_manager::bundled_models_response; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -111,10 +111,10 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 5e522c0aa0..0fd072a70f 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -5,10 +5,10 @@ use anyhow::Context; use anyhow::Result; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses; @@ -82,7 +82,10 @@ async fn tool_call_output_configured_limit_chars_type() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -156,7 +159,10 @@ async fn tool_call_output_exceeds_limit_truncated_chars_limit() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -229,7 +235,10 @@ async fn tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -303,7 +312,10 @@ async fn tool_call_output_truncated_only_once() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; let output = mock2 @@ -399,9 +411,9 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> let fixture = builder.build(&server).await?; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "call the rmcp echo tool with a very large message", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -496,6 +508,8 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { }); let fixture = builder.build(&server).await?; let session_model = fixture.session_configured.model.clone(); + let permission_profile = PermissionProfile::read_only(); + let sandbox_policy = permission_profile.to_legacy_sandbox_policy(fixture.cwd.path())?; fixture .codex @@ -509,8 +523,8 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile: Some(permission_profile), model: session_model, effort: None, summary: None, @@ -577,7 +591,7 @@ async fn token_policy_marker_reports_tokens() -> Result<()> { .await; fixture - .submit_turn_with_policy("run the shell tool", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile("run the shell tool", PermissionProfile::Disabled) .await?; let output = done_mock @@ -628,7 +642,7 @@ async fn byte_policy_marker_reports_bytes() -> Result<()> { .await; fixture - .submit_turn_with_policy("run the shell tool", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile("run the shell tool", PermissionProfile::Disabled) .await?; let output = done_mock @@ -680,9 +694,9 @@ async fn shell_command_output_not_truncated_with_custom_limit() -> Result<()> { .await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "run big output without truncation", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -777,9 +791,9 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { let fixture = builder.build(&server).await?; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "call the rmcp echo tool with a very large message", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; diff --git a/codex-rs/core/tests/suite/web_search.rs b/codex-rs/core/tests/suite/web_search.rs index d3b0f21930..065711a371 100644 --- a/codex-rs/core/tests/suite/web_search.rs +++ b/codex-rs/core/tests/suite/web_search.rs @@ -2,7 +2,7 @@ use codex_features::Feature; use codex_protocol::config_types::WebSearchMode; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use core_test_support::responses; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; @@ -44,9 +44,9 @@ async fn web_search_mode_cached_sets_external_web_access_false() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello cached web search", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -86,9 +86,9 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello cached+live flags", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -132,9 +132,9 @@ async fn web_search_mode_defaults_to_cached_when_features_disabled() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello default cached web search", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -149,7 +149,7 @@ async fn web_search_mode_defaults_to_cached_when_features_disabled() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_updates_between_turns_with_sandbox_policy() { +async fn web_search_mode_updates_between_turns_with_permission_profile() { skip_if_no_network!(); let server = start_mock_server().await; @@ -187,10 +187,10 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello cached", SandboxPolicy::new_read_only_policy()) + test.submit_turn_with_permission_profile("hello cached", PermissionProfile::read_only()) .await .expect("submit first turn"); - test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess) + test.submit_turn_with_permission_profile("hello live", PermissionProfile::Disabled) .await .expect("submit second turn"); @@ -248,9 +248,9 @@ location = { country = "US", city = "New York", timezone = "America/New_York" } .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello configured web search", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await .expect("submit turn"); diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index f685120132..2d2f9a41c1 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -23,6 +23,8 @@ use codex_protocol::user_input::UserInput; use serde::Serialize; use serde_json::Value; +use crate::permission_compat::legacy_compatible_permission_profile; + #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct AppCommand(Op); @@ -148,19 +150,12 @@ impl AppCommand { collaboration_mode: Option, personality: Option, ) -> Self { - let sandbox_policy = permission_profile + let legacy_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let sandbox_policy = legacy_profile .to_legacy_sandbox_policy(cwd.as_path()) .unwrap_or_else(|err| { - tracing::warn!( - %err, - "permission profile cannot be projected to legacy UserTurn sandbox; using read-only compatibility fallback" - ); - match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - unreachable!("read-only permissions must be legacy-compatible: {err}") - } - } + unreachable!("legacy-compatible permissions must project to legacy policy: {err}") }); Self(Op::UserTurn { items, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index e9a5fb447f..fb8f80267f 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3,6 +3,7 @@ use crate::bottom_pane::FeedbackAudience; use crate::legacy_core::append_message_history_entry; use crate::legacy_core::config::Config; use crate::legacy_core::message_history_metadata; +use crate::permission_compat::legacy_compatible_permission_profile; use crate::status::StatusAccountDisplay; use crate::status::plan_type_display_name; use codex_app_server_client::AppServerClient; @@ -542,21 +543,15 @@ impl AppServerSession { ) -> Result { let request_id = self.next_request_id(); let sandbox_policy = if matches!(self.thread_params_mode(), ThreadParamsMode::Remote) { - let policy = match permission_profile.to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - tracing::warn!( - %err, - "permission profile cannot be projected for remote turn/start; using read-only compatibility fallback" - ); - match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - unreachable!("read-only permissions must be legacy-compatible: {err}") - } - } - } - }; + let legacy_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let policy = legacy_profile + .to_legacy_sandbox_policy(cwd.as_path()) + .unwrap_or_else(|err| { + unreachable!( + "legacy-compatible permissions must project to legacy policy: {err}" + ) + }); Some(policy.into()) } else { None @@ -1123,7 +1118,10 @@ fn sandbox_mode_from_permission_profile( .network_sandbox_policy() .is_enabled() .then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess) - } else if file_system_policy.can_write_path_with_cwd(cwd, cwd) { + } else if !file_system_policy + .get_writable_roots_with_cwd(cwd) + .is_empty() + { Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) } else { Some(codex_app_server_protocol::SandboxMode::ReadOnly) @@ -1513,6 +1511,12 @@ mod tests { use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; + use codex_protocol::models::ManagedFileSystemPermissions; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; @@ -1605,6 +1609,35 @@ mod tests { assert_eq!(fork.permission_profile, None); } + #[test] + fn sandbox_mode_preserves_non_cwd_write_roots_for_remote_sessions() { + let cwd = test_path_buf("/workspace/project").abs(); + let extra_root = test_path_buf("/workspace/cache").abs(); + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: extra_root }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + + assert_eq!( + sandbox_mode_from_permission_profile(&permission_profile, cwd.as_path()), + Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) + ); + } + #[tokio::test] async fn thread_lifecycle_params_forward_explicit_remote_cwd_override_for_remote_sessions() { let temp_dir = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0010fa7035..81c4ee4445 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -148,6 +148,7 @@ mod npm_registry; pub(crate) mod onboarding; mod oss_selection; mod pager_overlay; +mod permission_compat; pub(crate) mod public_widgets; mod render; mod resize_reflow_cap; diff --git a/codex-rs/tui/src/permission_compat.rs b/codex-rs/tui/src/permission_compat.rs new file mode 100644 index 0000000000..2debbd2745 --- /dev/null +++ b/codex-rs/tui/src/permission_compat.rs @@ -0,0 +1,102 @@ +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::path::Path; + +pub(crate) fn legacy_compatible_permission_profile( + permission_profile: &PermissionProfile, + cwd: &Path, +) -> PermissionProfile { + if permission_profile.to_legacy_sandbox_policy(cwd).is_ok() { + return permission_profile.clone(); + } + + let file_system_policy = permission_profile.file_system_sandbox_policy(); + compatibility_workspace_write_profile( + &file_system_policy, + permission_profile.network_sandbox_policy(), + cwd, + ) +} + +fn compatibility_workspace_write_profile( + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: &Path, +) -> PermissionProfile { + let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); + let writable_roots = file_system_policy + .get_writable_roots_with_cwd(cwd) + .into_iter() + .map(|root| root.root) + .filter(|root| cwd_abs.as_ref() != Some(root)) + .collect::>(); + let tmpdir_writable = std::env::var_os("TMPDIR") + .filter(|tmpdir| !tmpdir.is_empty()) + .and_then(|tmpdir| { + AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok() + }) + .is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd)); + let slash_tmp = Path::new("/tmp"); + let slash_tmp_writable = slash_tmp.is_absolute() + && slash_tmp.is_dir() + && file_system_policy.can_write_path_with_cwd(slash_tmp, cwd); + + PermissionProfile::workspace_write_with( + &writable_roots, + network_policy, + !tmpdir_writable, + !slash_tmp_writable, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_protocol::models::ManagedFileSystemPermissions; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; + use pretty_assertions::assert_eq; + + #[test] + fn compatibility_profile_preserves_unbridgeable_write_roots() { + let cwd = AbsolutePathBuf::try_from("/workspace/project").expect("absolute cwd"); + let extra_root = AbsolutePathBuf::try_from("/workspace/extra").expect("absolute root"); + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: extra_root.clone(), + }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + + let compatibility_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let policy = compatibility_profile + .to_legacy_sandbox_policy(cwd.as_path()) + .expect("compatibility profile should project to legacy policy"); + let roots = policy + .get_writable_roots_with_cwd(cwd.as_path()) + .into_iter() + .map(|root| root.root) + .collect::>(); + + assert_eq!(roots, vec![extra_root, cwd]); + } +} From 1d67aaa26c61f0d10230d4a38bd41be4cbd4881d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 09:03:15 -0700 Subject: [PATCH 2/5] core tests: build user turns from permission profiles --- codex-rs/core/tests/common/test_codex.rs | 19 +++++++++++++++---- codex-rs/core/tests/suite/image_rollout.rs | 15 ++++++++++----- codex-rs/core/tests/suite/json_result.rs | 9 ++++++--- codex-rs/core/tests/suite/live_reload.rs | 9 ++++++--- codex-rs/core/tests/suite/models_cache_ttl.rs | 9 ++++++--- codex-rs/core/tests/suite/pending_input.rs | 9 ++++++--- .../core/tests/suite/request_user_input.rs | 15 ++++++++++----- codex-rs/core/tests/suite/skills.rs | 9 ++++++--- codex-rs/core/tests/suite/tool_parallelism.rs | 15 ++++++++++----- .../core/tests/suite/websocket_fallback.rs | 9 ++++++--- 10 files changed, 81 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index fd92d0fde1..57d50ebf69 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -203,6 +203,18 @@ pub enum ShellModelOutput { // UnifiedExec has its own set of tests } +/// Returns the permission fields required by `Op::UserTurn` for tests that +/// construct the op directly. +pub fn turn_permission_fields( + permission_profile: PermissionProfile, + cwd: &Path, +) -> (SandboxPolicy, Option) { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(cwd) + .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + (sandbox_policy, Some(permission_profile)) +} + pub struct TestCodexBuilder { config_mutators: Vec>, auth: CodexAuth, @@ -689,14 +701,13 @@ impl TestCodex { service_tier: Option>, environments: Option>, ) -> Result<()> { - let sandbox_policy = permission_profile - .to_legacy_sandbox_policy(self.config.cwd.as_path()) - .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, self.config.cwd.as_path()); self.submit_turn_with_context( prompt, approval_policy, sandbox_policy, - Some(permission_profile), + permission_profile, service_tier, environments, ) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 18ebe0fb03..f819e1cfdf 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -1,13 +1,13 @@ use anyhow::Context; use codex_protocol::models::ContentItem; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -18,6 +18,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use image::ImageBuffer; use image::Rgba; @@ -108,6 +109,8 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu responses::mount_sse_once(&server, response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -125,8 +128,8 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -196,6 +199,8 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> responses::mount_sse_once(&server, response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -213,8 +218,8 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 01783a365d..c4ce8ac908 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -1,14 +1,15 @@ #![cfg(not(target_os = "windows"))] +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use responses::ev_assistant_message; @@ -69,6 +70,8 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { responses::mount_sse_once_match(&server, match_json_text_param, sse1).await; let TestCodex { codex, cwd, .. } = test_codex().build(&server).await?; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); // 1) Normal user input – should hit server once. codex @@ -82,8 +85,8 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 25206a6fd3..c422073e4f 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -8,10 +8,10 @@ use std::time::Duration; use anyhow::Result; use codex_config::config_toml::ProjectConfig; use codex_protocol::config_types::TrustLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ResponsesRequest; @@ -19,6 +19,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use tokio::time::timeout; @@ -46,6 +47,8 @@ fn contains_skill_body(request: &ResponsesRequest, skill_body: &str) -> bool { async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) -> Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -63,8 +66,8 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 8eb8fca8ac..e2688afc97 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -9,6 +9,7 @@ use codex_login::CodexAuth; use codex_models_manager::client_version_to_whole; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelVisibility; @@ -19,7 +20,6 @@ use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -28,6 +28,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde::Deserialize; @@ -87,6 +88,8 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { sse_response(response_body).insert_header("X-Models-Etag", ETAG), ) .await; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); codex .submit(Op::UserTurn { @@ -99,8 +102,8 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { cwd: test.cwd_path().to_path_buf(), approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index cfe9ba8fcc..b582a2abc7 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use codex_core::CodexThread; use codex_protocol::AgentPath; use codex_protocol::items::TurnItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; @@ -25,6 +25,7 @@ use core_test_support::streaming_sse::StreamingSseServer; use core_test_support::streaming_sse::start_streaming_sse_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; @@ -108,6 +109,8 @@ async fn submit_user_input(codex: &CodexThread, text: &str) { } async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -119,8 +122,8 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { cwd: test.config.cwd.to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 7814f4936c..c82a5a1672 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -6,10 +6,10 @@ use codex_features::Feature; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -24,6 +24,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -128,6 +129,8 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -140,8 +143,8 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -248,6 +251,8 @@ where let session_model = session_configured.model.clone(); let collaboration_mode = build_mode(session_model.clone()); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -260,8 +265,8 @@ where cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 1edaefb783..ab5e462991 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -9,9 +9,9 @@ use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::ExecutorFileSystem; use codex_login::CodexAuth; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; @@ -24,6 +24,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -97,6 +98,8 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { .await; let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -114,8 +117,8 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { cwd: test.config.cwd.to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index a0ee7b1140..47282c283d 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -5,10 +5,10 @@ use std::fs; use std::time::Duration; use std::time::Instant; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -24,6 +24,7 @@ use core_test_support::streaming_sse::StreamingSseChunk; use core_test_support::streaming_sse::start_streaming_sse_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; @@ -32,6 +33,8 @@ use tokio::sync::oneshot; async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd.path()); test.codex .submit(Op::UserTurn { @@ -44,8 +47,8 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { cwd: test.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -352,6 +355,8 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an .await?; let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd.path()); test.codex .submit(Op::UserTurn { environments: None, @@ -363,8 +368,8 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an cwd: test.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index dbe2e3c6d7..f073ce7ab8 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -1,9 +1,9 @@ use anyhow::Result; use codex_model_provider_info::WireApi; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_completed; @@ -14,6 +14,7 @@ use core_test_support::responses::sse; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use tokio::time::Duration; use tokio::time::timeout; @@ -147,6 +148,8 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result cwd, .. } = builder.build(&server).await?; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -159,8 +162,8 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_configured.model.clone(), effort: None, summary: None, From dd28e0ac9b756dd7ac567efb0f2199ca9fdb8148 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 09:16:09 -0700 Subject: [PATCH 3/5] core tests: migrate more turns to permission profiles --- codex-rs/core/tests/suite/exec.rs | 6 +-- .../core/tests/suite/models_etag_responses.rs | 12 +++-- codex-rs/core/tests/suite/shell_snapshot.rs | 27 +++++++---- codex-rs/core/tests/suite/sqlite_state.rs | 12 +++-- codex-rs/core/tests/suite/tool_harness.rs | 48 ++++++++++++------- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index a0dd7f8e25..923d223e13 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -9,7 +9,6 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::Result; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use core_test_support::PathExt; @@ -50,12 +49,9 @@ where arg0: None, }; - let policy = SandboxPolicy::new_read_only_policy(); - let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&policy); - process_exec_tool_call( params, - &permission_profile, + &PermissionProfile::read_only(), &cwd, &None, /*use_legacy_landlock*/ false, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 0551dcf8e1..a9aa843ad1 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -6,11 +6,11 @@ use std::time::Duration; use anyhow::Result; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -21,6 +21,7 @@ use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event_with_timeout; use pretty_assertions::assert_eq; use wiremock::MockServer; @@ -61,6 +62,9 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch let codex = Arc::clone(&test.codex); let cwd = Arc::clone(&test.cwd); let session_model = test.session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); assert_eq!(spawn_models_mock.requests().len(), 1); assert_eq!(spawn_models_mock.single_request_path(), "/v1/models"); @@ -107,11 +111,11 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 1204f2b078..f757fa322a 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -1,11 +1,11 @@ use anyhow::Result; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -15,6 +15,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -154,6 +155,8 @@ async fn run_snapshot_command_with_options( let codex_home = test.home.path().to_path_buf(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { @@ -166,8 +169,8 @@ async fn run_snapshot_command_with_options( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -247,6 +250,8 @@ async fn run_shell_command_snapshot_with_options( let codex_home = test.home.path().to_path_buf(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { @@ -259,8 +264,8 @@ async fn run_shell_command_snapshot_with_options( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -321,6 +326,8 @@ async fn run_tool_turn_on_harness( let codex = test.codex.clone(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { environments: None, @@ -332,8 +339,8 @@ async fn run_tool_turn_on_harness( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -558,6 +565,8 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { mount_sse_sequence(harness.server(), responses).await; let model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { environments: None, @@ -569,8 +578,8 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { cwd: cwd.clone(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 8ad9ede5b9..8250f5493d 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -4,12 +4,12 @@ use codex_config::types::McpServerTransportConfig; use codex_features::Feature; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; @@ -26,6 +26,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -396,6 +397,9 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< let test = builder.build(&server).await?; let db = test.codex.state_db().expect("state db enabled"); let thread_id = test.session_configured.session_id; + let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd.as_path()); test.codex .submit(Op::UserTurn { @@ -405,11 +409,11 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index db93c0b66e..62d6dcef90 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -4,11 +4,11 @@ use std::fs; use assert_matches::assert_matches; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::plan_tool::StepStatus; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses; @@ -24,6 +24,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use serde_json::Value; use serde_json::json; @@ -75,6 +76,9 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -84,11 +88,11 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -148,6 +152,9 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -157,11 +164,11 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -231,6 +238,9 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -240,11 +250,11 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -329,6 +339,9 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -338,11 +351,11 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -435,6 +448,9 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -444,11 +460,11 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, From 084e2c4c4feaa7e53912e445e58972c12de3be6a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 09:23:42 -0700 Subject: [PATCH 4/5] core tests: configure profiles directly --- codex-rs/core/tests/suite/code_mode.rs | 13 ++++-- codex-rs/core/tests/suite/codex_delegate.rs | 12 +++--- .../core/tests/suite/model_visible_layout.rs | 40 +++++++++++++------ codex-rs/core/tests/suite/otel.rs | 7 ++-- codex-rs/core/tests/suite/tools.rs | 38 +++++++++--------- codex-rs/core/tests/suite/user_shell_cmd.rs | 16 +++++--- 6 files changed, 78 insertions(+), 48 deletions(-) diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 413fd90bbb..af94252c02 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -11,10 +11,10 @@ use codex_models_manager::bundled_models_response; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::assert_regex_match; @@ -30,6 +30,7 @@ use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -2607,6 +2608,10 @@ text( ) .await; + let cwd = test.cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + test.codex .submit(Op::UserTurn { environments: None, @@ -2615,11 +2620,11 @@ text( text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index 461b07284c..2ccb6fecba 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -1,12 +1,12 @@ use codex_core::config::Constrained; use codex_core::sandboxing::SandboxPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::responses::ev_apply_patch_function_call; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -65,8 +65,9 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config - .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::read_only()) + .expect("set permission profile"); }); let test = builder.build(&server).await.expect("build test codex"); @@ -149,8 +150,9 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); // Use a restricted sandbox so patch approval is required config - .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::read_only()) + .expect("set permission profile"); config.include_apply_patch_tool = true; }); let test = builder.build(&server).await.expect("build test codex"); diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 2f06d5af21..7ae148788e 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -6,10 +6,10 @@ use std::sync::Arc; use anyhow::Result; use codex_config::types::Personality; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; @@ -24,6 +24,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use serde_json::json; @@ -111,6 +112,9 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { let test = builder.build(&server).await?; let preturn_context_diff_cwd = test.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD); fs::create_dir_all(&preturn_context_diff_cwd)?; + let first_turn_cwd = test.cwd_path().to_path_buf(); + let (first_sandbox_policy, first_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), first_turn_cwd.as_path()); test.codex .submit(Op::UserTurn { @@ -120,11 +124,11 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), + cwd: first_turn_cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: first_sandbox_policy, + permission_profile: first_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -138,6 +142,10 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { }) .await; + let (second_sandbox_policy, second_permission_profile) = turn_permission_fields( + PermissionProfile::read_only(), + preturn_context_diff_cwd.as_path(), + ); test.codex .submit(Op::UserTurn { environments: None, @@ -149,8 +157,8 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { cwd: preturn_context_diff_cwd, approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: second_sandbox_policy, + permission_profile: second_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -218,6 +226,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd_two.join("AGENTS.md"), "# AGENTS two\n\n\nTurn two agents instructions.\n\n", )?; + let (first_sandbox_policy, first_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd_one.as_path()); test.codex .submit(Op::UserTurn { @@ -230,8 +240,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd: cwd_one.clone(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: first_sandbox_policy, + permission_profile: first_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -245,6 +255,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R }) .await; + let (second_sandbox_policy, second_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd_two.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -256,8 +268,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd: cwd_two, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: second_sandbox_policy, + permission_profile: second_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -358,6 +370,10 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul let resumed = resume_builder.resume(&server, home, rollout_path).await?; let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD); fs::create_dir_all(&resume_override_cwd)?; + let (sandbox_policy, permission_profile) = turn_permission_fields( + PermissionProfile::read_only(), + resume_override_cwd.as_path(), + ); resumed .codex .submit(Op::UserTurn { @@ -370,8 +386,8 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul cwd: resume_override_cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile, model: resumed.session_configured.model.clone(), effort: resumed.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 3d2f5102e2..5bf41bd547 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -1,10 +1,10 @@ use codex_core::config::Constrained; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -1111,8 +1111,9 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { .with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index aff8755b11..46195e4dd0 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -190,10 +190,10 @@ async fn custom_tool_unknown_returns_custom_output_error() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "invoke custom tool", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -408,10 +408,10 @@ async fn shell_escalated_permissions_rejected_then_ok() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run the shell command", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -488,9 +488,9 @@ async fn sandbox_denied_shell_returns_original_output() -> Result<()> { let mock = mount_sse_sequence(&server, responses).await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "run a command that should be denied by the read-only sandbox", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -675,10 +675,10 @@ async fn collect_tools(use_unified_exec: bool) -> Result> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -746,10 +746,10 @@ async fn shell_timeout_includes_timeout_prefix_and_metadata() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run a long command", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -791,8 +791,9 @@ async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }); let test = builder.build(&server).await?; @@ -837,10 +838,10 @@ time.sleep(60) let start = Instant::now(); let output_str = tokio::time::timeout(Duration::from_secs(10), async { - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run a command with a detached grandchild", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; let timeout_item = second_mock.single_request().function_call_output(call_id); @@ -885,8 +886,9 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(|cfg| { - cfg.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + cfg.permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }); let test = builder.build(&server).await?; @@ -922,10 +924,10 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "spawn a missing binary", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 5526356693..1285b9f925 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -8,7 +8,6 @@ use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ExecOutputStream; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::user_input::UserInput; use core_test_support::PathBufExt; @@ -23,6 +22,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use core_test_support::wait_for_event_with_timeout; @@ -168,6 +168,10 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> ]); let mock = responses::mount_sse_sequence(&server, vec![first, second]).await; + let cwd = fixture.cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + fixture .codex .submit(Op::UserTurn { @@ -177,11 +181,11 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: fixture.session_configured.model.clone(), effort: None, summary: None, @@ -484,9 +488,9 @@ async fn user_shell_command_is_truncated_only_once() -> anyhow::Result<()> { .await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "trigger big shell_command output", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; From bb39f8104055ea91b2e4799064692b28dc5681af Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 09:25:51 -0700 Subject: [PATCH 5/5] core tests: send model turns with permission profiles --- codex-rs/core/tests/suite/remote_models.rs | 21 ++++++++++++------- .../suite/responses_api_proxy_headers.rs | 17 +++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index a69dae4a5b..49218c78d2 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -8,6 +8,7 @@ use codex_models_manager::bundled_models_response; use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::SharedModelsManager; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; @@ -21,7 +22,6 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; use core_test_support::responses::ev_assistant_message; @@ -37,6 +37,7 @@ use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -627,6 +628,9 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { ]; mount_sse_sequence(&server, responses).await; + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { items: vec![UserInput::Text { @@ -634,11 +638,11 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: REMOTE_MODEL_SLUG.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), @@ -855,6 +859,9 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { }) .await?; + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { items: vec![UserInput::Text { @@ -862,11 +869,11 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: model.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index cd9a73696d..09e99f78c3 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -4,10 +4,10 @@ use anyhow::Result; use anyhow::anyhow; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponsesRequest; @@ -21,6 +21,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use serde_json::json; use std::time::Duration; @@ -125,6 +126,9 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() - async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> { let session_model = test.session_configured.model.clone(); + let cwd = test.config.cwd.to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -133,16 +137,11 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.config.cwd.to_path_buf(), + cwd, approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None,