diff --git a/codex-rs/core/tests/common/zsh_fork.rs b/codex-rs/core/tests/common/zsh_fork.rs index 448693e069..e58ebd81dd 100644 --- a/codex-rs/core/tests/common/zsh_fork.rs +++ b/codex-rs/core/tests/common/zsh_fork.rs @@ -5,8 +5,9 @@ use anyhow::Result; use codex_core::config::Config; use codex_core::config::Constrained; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use crate::test_codex::TestCodex; use crate::test_codex::test_codex; @@ -22,7 +23,7 @@ impl ZshForkRuntime { &self, config: &mut Config, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, ) { config .features @@ -37,18 +38,19 @@ impl ZshForkRuntime { config.permissions.allow_login_shell = false; config.permissions.approval_policy = Constrained::allow_any(approval_policy); config - .set_legacy_sandbox_policy(sandbox_policy) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(permission_profile) + .expect("set permission profile"); } } -pub fn restrictive_workspace_write_policy() -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - } +pub fn restrictive_workspace_write_profile() -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ) } pub fn zsh_fork_runtime(test_name: &str) -> Result> { @@ -78,7 +80,7 @@ pub async fn build_zsh_fork_test( server: &wiremock::MockServer, runtime: ZshForkRuntime, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, pre_build_hook: F, ) -> Result where @@ -87,7 +89,7 @@ where let mut builder = test_codex() .with_pre_build_hook(pre_build_hook) .with_config(move |config| { - runtime.apply_to_config(config, approval_policy, sandbox_policy); + runtime.apply_to_config(config, approval_policy, permission_profile); }); builder.build(server).await } diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 4bb3be6631..2d931efb38 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -13,11 +13,9 @@ use std::sync::atomic::Ordering; use std::time::Duration; use codex_features::Feature; -use codex_protocol::protocol::AskForApproval; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; -use codex_protocol::user_input::UserInput; #[cfg(target_os = "linux")] use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use core_test_support::assert_regex_match; @@ -59,6 +57,15 @@ async fn apply_patch_harness_with( Box::pin(TestCodexHarness::with_remote_aware_builder(builder)).await } +fn restrictive_workspace_write_profile() -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ) +} + pub async fn mount_apply_patch( harness: &TestCodexHarness, call_id: &str, @@ -354,28 +361,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( let call_id = "apply-move-no-change"; mount_apply_patch(&harness, call_id, patch, "ok", model_output).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "rename without content change".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("rename without content change").await?; let mut saw_turn_diff = false; wait_for_event(&codex, |event| match event { @@ -641,16 +627,10 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace( let call_id = "apply-path-traversal"; mount_apply_patch(&harness, call_id, patch, "fail", model_output).await; - let sandbox_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; harness - .submit_with_policy( + .submit_with_permission_profile( "attempt to escape workspace via apply_patch", - sandbox_policy, + restrictive_workspace_write_profile(), ) .await?; @@ -696,14 +676,11 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace( let call_id = "apply-move-traversal"; mount_apply_patch(&harness, call_id, patch, "fail", model_output).await; - let sandbox_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; harness - .submit_with_policy("attempt move traversal via apply_patch", sandbox_policy) + .submit_with_permission_profile( + "attempt move traversal via apply_patch", + restrictive_workspace_write_profile(), + ) .await?; let out = harness.apply_patch_output(call_id, model_output).await; @@ -992,27 +969,7 @@ async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()> ) .await; - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "create streamed file".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: test.session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("create streamed file").await?; let mut updates = Vec::new(); wait_for_event(&codex, |event| match event { @@ -1090,28 +1047,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( ]; mount_sse_sequence(harness.server(), bodies).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "apply via shell heredoc with cd".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("apply via shell heredoc with cd").await?; let mut saw_turn_diff = None; let mut saw_patch_begin = false; @@ -1176,28 +1112,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> ]; mount_sse_sequence(harness.server(), bodies).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "apply patch via shell".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("apply patch via shell").await?; let mut saw_turn_diff = false; wait_for_event(&codex, |event| match event { @@ -1333,28 +1248,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( let patch = format!("*** Begin Patch\n*** Add File: {file}\n+hello\n*** End Patch\n"); mount_apply_patch(&harness, call_id, patch.as_str(), "ok", model_output).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "emit diff".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("emit diff").await?; let mut saw_turn_diff = None; wait_for_event(&codex, |event| match event { @@ -1402,28 +1296,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( let patch = "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch"; mount_apply_patch(&harness, call_id, patch, "ok", model_output).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "rename with change".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("rename with change").await?; let mut last_diff: Option = None; wait_for_event(&codex, |event| match event { @@ -1480,28 +1353,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> ]); mount_sse_sequence(harness.server(), vec![s1, s2, s3]).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "aggregate diffs".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("aggregate diffs").await?; let mut last_diff: Option = None; wait_for_event(&codex, |event| match event { @@ -1558,28 +1410,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result ]; mount_sse_sequence(harness.server(), responses).await; - let model = test.session_configured.model.clone(); - codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "apply patch twice with failure".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: harness.cwd().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - .await?; + harness.submit("apply patch twice with failure").await?; let mut last_diff: Option = None; wait_for_event_with_timeout( diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index ef22cfbfc1..79bf0f830f 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -32,10 +32,11 @@ 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_with_timeout; use core_test_support::zsh_fork::build_zsh_fork_test; -use core_test_support::zsh_fork::restrictive_workspace_write_policy; +use core_test_support::zsh_fork::restrictive_workspace_write_profile; use core_test_support::zsh_fork::zsh_fork_runtime; use pretty_assertions::assert_eq; use regex_lite::Regex; @@ -2318,7 +2319,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> { }; let approval_policy = AskForApproval::Never; - let sandbox_policy = restrictive_workspace_write_policy(); + let permission_profile = restrictive_workspace_write_profile(); let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?; let outside_path = outside_dir .path() @@ -2332,7 +2333,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> { &server, runtime, approval_policy, - sandbox_policy.clone(), + permission_profile.clone(), move |home| { let _ = fs::remove_file(&outside_path_for_hook); let rules_dir = home.join("rules"); @@ -2367,13 +2368,30 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> { ) .await; - submit_turn( - &test, - "run allowed touch under zsh fork", - approval_policy, - sandbox_policy, - ) - .await?; + let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, test.cwd.path()); + test.codex + .submit(Op::UserTurn { + environments: None, + items: vec![UserInput::Text { + text: "run allowed touch under zsh fork".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd.path().to_path_buf(), + approval_policy, + approvals_reviewer: Some(ApprovalsReviewer::User), + sandbox_policy, + permission_profile, + model: session_model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; wait_for_completion_without_approval(&test).await; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index ce9dfdf28c..629c370103 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -8,6 +8,7 @@ use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::built_in_model_providers; use codex_models_manager::bundled_models_response; use codex_protocol::items::TurnItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; @@ -17,7 +18,6 @@ use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::WarningEvent; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; @@ -28,6 +28,7 @@ use core_test_support::responses::ev_reasoning_item; use core_test_support::responses::mount_models_once; 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 std::path::PathBuf; @@ -71,6 +72,30 @@ const PRETURN_CONTEXT_DIFF_CWD: &str = "/tmp/PRETURN_CONTEXT_DIFF_CWD"; pub(super) const COMPACT_WARNING_MESSAGE: &str = "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted."; +fn disabled_permission_user_turn(text: impl Into, cwd: PathBuf, model: String) -> Op { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + Op::UserTurn { + environments: None, + items: vec![UserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd, + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + } +} + fn auto_summary(summary: &str) -> String { summary.to_string() } @@ -1675,25 +1700,11 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { resumed .codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: follow_up_user.into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: resumed.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: resumed.session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + follow_up_user, + resumed.cwd.path().to_path_buf(), + resumed.session_configured.model.clone(), + )) .await .unwrap(); @@ -1768,25 +1779,11 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { let test = builder.build(&server).await.expect("build test codex"); test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "before switch".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: previous_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "before switch", + test.cwd.path().to_path_buf(), + previous_model.to_string(), + )) .await .expect("submit first user turn"); wait_for_event(&test.codex, |event| { @@ -1795,25 +1792,11 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "after switch".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: next_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "after switch", + test.cwd.path().to_path_buf(), + next_model.to_string(), + )) .await .expect("submit second user turn"); assert_compaction_uses_turn_lifecycle_id(&test.codex).await; @@ -1908,25 +1891,11 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { initial .codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "before resume".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: initial.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: previous_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "before resume", + initial.cwd.path().to_path_buf(), + previous_model.to_string(), + )) .await .expect("submit pre-resume turn"); wait_for_event(&initial.codex, |event| { @@ -1959,25 +1928,11 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { resumed .codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "after resume".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: resumed.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: next_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "after resume", + resumed.cwd.path().to_path_buf(), + next_model.to_string(), + )) .await .expect("submit resumed user turn"); assert_compaction_uses_turn_lifecycle_id(&resumed.codex).await; @@ -3195,25 +3150,11 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch .expect("build codex"); test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "BEFORE_SWITCH_USER".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: previous_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "BEFORE_SWITCH_USER", + test.cwd.path().to_path_buf(), + previous_model.to_string(), + )) .await .expect("submit first user turn"); wait_for_event(&test.codex, |event| { @@ -3222,25 +3163,11 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "AFTER_SWITCH_USER".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: next_model.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_permission_user_turn( + "AFTER_SWITCH_USER", + test.cwd.path().to_path_buf(), + next_model.to_string(), + )) .await .expect("submit second user turn"); wait_for_event(&test.codex, |event| { diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 4fae6aafdf..20fef45763 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -5,10 +5,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::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -18,6 +18,7 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; 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; @@ -38,10 +39,12 @@ async fn submit_user_turn( test: &core_test_support::test_codex::TestCodex, prompt: &str, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, collaboration_mode: Option, ) -> Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -54,7 +57,7 @@ async fn submit_user_turn( approval_policy, approvals_reviewer: None, sandbox_policy, - permission_profile: None, + permission_profile, model: session_model, effort: None, summary: None, @@ -125,6 +128,8 @@ async fn execpolicy_blocks_shell_invocation() -> 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, @@ -136,8 +141,8 @@ async fn execpolicy_blocks_shell_invocation() -> 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, @@ -208,7 +213,7 @@ async fn shell_command_empty_script_with_collaboration_mode_does_not_panic() -> &test, "run an empty shell command", AskForApproval::OnRequest, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; @@ -267,7 +272,7 @@ async fn unified_exec_empty_script_with_collaboration_mode_does_not_panic() -> R &test, "run empty unified exec command", AskForApproval::OnRequest, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; @@ -322,7 +327,7 @@ async fn shell_command_whitespace_script_with_collaboration_mode_does_not_panic( &test, "run whitespace shell command", AskForApproval::OnRequest, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; @@ -381,7 +386,7 @@ async fn unified_exec_whitespace_script_with_collaboration_mode_does_not_panic() &test, "run whitespace unified exec command", AskForApproval::OnRequest, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 6d232afd21..daf3053adc 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -7,13 +7,14 @@ use codex_core::config::Constrained; use codex_features::Feature; use codex_protocol::items::parse_hook_prompt_fragment; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; +use codex_protocol::permissions::NetworkSandboxPolicy; 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::managed_network_requirements_loader; use core_test_support::responses::ev_apply_patch_function_call; @@ -48,6 +49,24 @@ const BLOCKED_PROMPT_CONTEXT: &str = "Remember the blocked lighthouse note."; const PERMISSION_REQUEST_HOOK_MATCHER: &str = "^Bash$"; const PERMISSION_REQUEST_ALLOW_REASON: &str = "should not be used for allow"; +fn restrictive_workspace_write_profile() -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ) +} + +fn network_workspace_write_profile() -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ) +} + fn write_stop_hook(home: &Path, block_prompts: &[&str]) -> Result<()> { let script_path = home.join("stop_hook.py"); let log_path = home.join("stop_hook_log.jsonl"); @@ -1322,10 +1341,10 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() -> fs::write(&marker, "seed").context("create permission request marker")?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run the shell command after hook approval", AskForApproval::OnRequest, - codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -1407,15 +1426,10 @@ async fn permission_request_hook_allows_apply_patch_with_write_alias() -> Result let test = builder.build(&server).await?; let target_path = test.workspace_path(&patch_path); - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "apply the patch after hook approval", AskForApproval::OnRequest, - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }, + restrictive_workspace_write_profile(), ) .await?; @@ -1494,10 +1508,10 @@ async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> { fs::write(&marker, "seed").context("create exec command permission request marker")?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run the exec command after hook approval", AskForApproval::OnRequest, - codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -1558,13 +1572,8 @@ allow_local_binding = true .await; let approval_policy = AskForApproval::OnFailure; - let sandbox_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; - let sandbox_policy_for_config = sandbox_policy.clone(); + let permission_profile = network_workspace_write_profile(); + let permission_profile_for_config = permission_profile.clone(); let test = test_codex() .with_home(Arc::clone(&home)) .with_pre_build_hook(|home| { @@ -1580,8 +1589,9 @@ allow_local_binding = true .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(approval_policy); config - .set_legacy_sandbox_policy(sandbox_policy_for_config) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(permission_profile_for_config) + .expect("set permission profile"); }) .build(&server) .await?; @@ -1598,10 +1608,10 @@ allow_local_binding = true .as_ref() .expect("expected runtime managed network proxy addresses"); - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run the shell command after network hook approval", approval_policy, - sandbox_policy, + permission_profile, ) .await?; @@ -1695,10 +1705,10 @@ async fn permission_request_hook_sees_retry_context_after_sandbox_denial() -> Re let marker_path = test.workspace_path(marker); let _ = fs::remove_file(&marker_path); - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "retry the shell command after sandbox denial", AskForApproval::OnFailure, - codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -1769,9 +1779,9 @@ async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> { fs::remove_file(&marker).context("remove leftover pre tool use marker")?; } - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the blocked shell command", - codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -2013,9 +2023,9 @@ async fn pre_tool_use_blocks_shell_when_defined_in_config_toml() -> Result<()> { fs::remove_file(&marker).context("remove leftover config.toml marker")?; } - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the blocked shell command from config toml", - codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index db4f7d7584..053f03ca90 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -6,7 +6,9 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::TurnItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::WebSearchAction; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemStartedEvent; @@ -33,12 +35,42 @@ 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; use std::path::Path; use std::path::PathBuf; +fn disabled_plan_turn( + text: &str, + model: String, + collaboration_mode: CollaborationMode, +) -> anyhow::Result { + let cwd = std::env::current_dir()?; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + Ok(Op::UserTurn { + environments: None, + items: vec![UserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd, + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: Some(collaboration_mode), + personality: None, + }) +} + fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf { fn sanitize(value: &str) -> String { let mut sanitized: String = value @@ -529,25 +561,11 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result< }; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "please plan".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: std::env::current_dir()?, - approval_policy: codex_protocol::protocol::AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: Some(collaboration_mode), - personality: None, - }) + .submit(disabled_plan_turn( + "please plan", + session_configured.model.clone(), + collaboration_mode, + )?) .await?; let plan_delta = wait_for_event_match(&codex, |ev| match ev { @@ -608,25 +626,11 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { }; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "please plan".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: std::env::current_dir()?, - approval_policy: codex_protocol::protocol::AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: Some(collaboration_mode), - personality: None, - }) + .submit(disabled_plan_turn( + "please plan", + session_configured.model.clone(), + collaboration_mode, + )?) .await?; let mut agent_deltas = Vec::new(); @@ -719,25 +723,11 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done }; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "please plan with citations".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: std::env::current_dir()?, - approval_policy: codex_protocol::protocol::AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: Some(collaboration_mode), - personality: None, - }) + .submit(disabled_plan_turn( + "please plan with citations", + session_configured.model.clone(), + collaboration_mode, + )?) .await?; let mut agent_started = None; @@ -908,25 +898,11 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p }; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "please plan".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: std::env::current_dir()?, - approval_policy: codex_protocol::protocol::AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: Some(collaboration_mode), - personality: None, - }) + .submit(disabled_plan_turn( + "please plan", + session_configured.model.clone(), + collaboration_mode, + )?) .await?; let mut agent_started = None; @@ -1024,25 +1000,11 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { }; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "please plan".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: std::env::current_dir()?, - approval_policy: codex_protocol::protocol::AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_configured.model.clone(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: Some(collaboration_mode), - personality: None, - }) + .submit(disabled_plan_turn( + "please plan", + session_configured.model.clone(), + collaboration_mode, + )?) .await?; let mut plan_delta = None; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b3e9215635..43ec50746e 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -5,6 +5,7 @@ use codex_login::CodexAuth; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; @@ -17,7 +18,6 @@ use codex_protocol::openai_models::default_input_modalities; 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_completed_with_tokens; use core_test_support::responses::ev_image_generation_call; @@ -29,13 +29,36 @@ use core_test_support::responses::sse; use core_test_support::responses::sse_completed; 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 pretty_assertions::assert_eq; use std::path::Path; use std::path::PathBuf; use wiremock::MockServer; +fn read_only_user_turn(test: &TestCodex, items: Vec, model: String) -> Op { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), test.cwd_path()); + Op::UserTurn { + environments: None, + items, + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: test.config.model_reasoning_effort, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + } +} + fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf { fn sanitize(value: &str) -> String { let mut sanitized: String = value @@ -120,25 +143,14 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( let next_model = "gpt-5.4"; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + test.session_configured.model.clone(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -160,25 +172,14 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "switch models".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: next_model.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + next_model.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -222,25 +223,14 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul let next_model = "exp-codex-personality"; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + test.session_configured.model.clone(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -262,25 +252,14 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "switch model and personality".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: next_model.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + next_model.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -401,9 +380,9 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< .to_string(); test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![ + .submit(read_only_user_turn( + &test, + vec![ UserInput::Image { image_url: image_url.clone(), }, @@ -412,42 +391,20 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< text_elements: Vec::new(), }, ], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: text_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + text_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -539,48 +496,26 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "generate a lobster".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "describe the generated image".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -675,48 +610,26 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "generate a lobster".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "describe the generated image".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: text_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + text_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -813,25 +726,14 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "generate a lobster".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -844,25 +746,14 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() .await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "after rollback".to_string(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: image_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + image_model_slug.to_string(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1003,25 +894,14 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< ); test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "use larger model".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: large_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + large_model_slug.to_string(), + )) .await?; let large_window_event = wait_for_event(&test.codex, |event| { @@ -1065,25 +945,14 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(read_only_user_turn( + &test, + vec![UserInput::Text { text: "switch to smaller model".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: smaller_model_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + smaller_model_slug.to_string(), + )) .await?; let smaller_turn_started_event = wait_for_event(&test.codex, |event| { diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 1380f6162f..8ec48e4b14 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -5,10 +5,11 @@ use codex_core::config::Constrained; use codex_core::context::ContextualUserFragment; use codex_core::context::PermissionsInstructions; use codex_core::load_exec_policy; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; 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 codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::ResponsesRequest; @@ -540,20 +541,19 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { .await; let writable = TempDir::new()?; let writable_root = AbsolutePathBuf::try_from(writable.path())?; - let sandbox_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable_root], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; - let sandbox_policy_for_config = sandbox_policy.clone(); + let permission_profile = PermissionProfile::workspace_write_with( + &[writable_root], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config .permissions - .set_legacy_sandbox_policy(sandbox_policy_for_config, config.cwd.as_path()) - .expect("test sandbox policy should be allowed"); + .set_permission_profile(permission_profile) + .expect("test permission profile should be allowed"); config.config_layer_stack = ConfigLayerStack::default(); }); let test = builder.build(&server).await?; @@ -574,6 +574,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let permissions = permissions_texts(&req.single_request()); let normalize_line_endings = |s: &str| s.replace("\r\n", "\n"); let exec_policy = load_exec_policy(&test.config.config_layer_stack).await?; + let sandbox_policy = test.config.legacy_sandbox_policy(); let expected = PermissionsInstructions::from_policy( &sandbox_policy, AskForApproval::OnRequest, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index a53242c0dd..dde6d2ca51 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -3,6 +3,7 @@ use codex_features::Feature; 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::ModelInstructionsVariables; @@ -16,7 +17,6 @@ use codex_protocol::openai_models::default_input_modalities; 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::load_default_config_for_test; use core_test_support::responses::mount_models_once; @@ -25,7 +25,9 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse_completed; 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 pretty_assertions::assert_eq; use tempfile::TempDir; @@ -39,6 +41,46 @@ const LOCAL_FRIENDLY_TEMPLATE: &str = "You optimize for team morale and being a supportive teammate as much as code quality."; const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer."; +fn read_only_text_turn( + test: &TestCodex, + text: &str, + model: String, + approval_policy: AskForApproval, +) -> Op { + let personality = None; + read_only_text_turn_with_personality(test, text, model, approval_policy, personality) +} + +fn read_only_text_turn_with_personality( + test: &TestCodex, + text: &str, + model: String, + approval_policy: AskForApproval, + personality: Option, +) -> Op { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), test.cwd_path()); + Op::UserTurn { + environments: None, + items: vec![UserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: test.config.model_reasoning_effort, + summary: None, + service_tier: None, + collaboration_mode: None, + personality, + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn personality_does_not_mutate_base_instructions_without_template() { let codex_home = TempDir::new().expect("create temp dir"); @@ -94,25 +136,12 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -147,25 +176,12 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -207,25 +223,12 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -273,25 +276,12 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -327,25 +317,12 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -368,25 +345,12 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -437,25 +401,12 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -478,25 +429,12 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -560,25 +498,12 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -601,25 +526,12 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.permissions.approval_policy.value(), - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: test.session_configured.model.clone(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + test.session_configured.model.clone(), + test.config.permissions.approval_policy.value(), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -722,25 +634,13 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: remote_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: Some(Personality::Friendly), - }) + .submit(read_only_text_turn_with_personality( + &test, + "hello", + remote_slug.to_string(), + AskForApproval::Never, + Some(Personality::Friendly), + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -845,25 +745,12 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: remote_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + remote_slug.to_string(), + AskForApproval::Never, + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -886,25 +773,12 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - .await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: remote_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(read_only_text_turn( + &test, + "hello", + remote_slug.to_string(), + AskForApproval::Never, + )) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 3852918c53..12f4ab76aa 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -9,12 +9,13 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; 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::TempDirExt; use core_test_support::responses::ev_completed; @@ -25,6 +26,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 pretty_assertions::assert_eq; use tempfile::TempDir; @@ -405,7 +407,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an ) .await; - let TestCodex { codex, .. } = test_codex() + let TestCodex { codex, config, .. } = test_codex() .with_config(|config| { config.user_instructions = Some("be consistent and helpful".to_string()); config @@ -431,19 +433,22 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let writable = TempDir::new().unwrap(); - let new_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable.path().try_into().unwrap()], - network_access: true, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = PermissionProfile::workspace_write_with( + &[writable.abs()], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(config.cwd.as_path()) + .expect("workspace profile should have legacy projection"); codex .submit(Op::OverrideTurnContext { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, - sandbox_policy: Some(new_policy.clone()), - permission_profile: None, + sandbox_policy: Some(sandbox_policy), + permission_profile: Some(permission_profile), windows_sandbox_level: None, model: None, effort: Some(Some(ReasoningEffort::High)), @@ -709,12 +714,14 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res // Second turn using per-turn overrides via UserTurn let new_cwd = TempDir::new().unwrap(); let writable = TempDir::new().unwrap(); - let new_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable.abs()], - network_access: true, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = PermissionProfile::workspace_write_with( + &[writable.abs()], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, new_cwd.path()); codex .submit(Op::UserTurn { environments: None, @@ -725,8 +732,8 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res cwd: new_cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: new_policy.clone(), - permission_profile: None, + sandbox_policy, + permission_profile, model: "o3".to_string(), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), @@ -983,6 +990,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path()); codex .submit(Op::UserTurn { environments: None, @@ -993,8 +1002,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu cwd: default_cwd.to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: "o3".to_string(), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), 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/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 8baf14293c..7a24e91bd4 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -6,11 +6,12 @@ use codex_core::config::Constrained; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; 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::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionsResponse; @@ -28,6 +29,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 pretty_assertions::assert_eq; use regex_lite::Regex; @@ -70,13 +72,13 @@ fn build_add_file_patch(patch_path: &Path, content: &str) -> String { ) } -fn workspace_write_excluding_tmp() -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - } +fn workspace_write_excluding_tmp() -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ) } fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile { @@ -133,10 +135,12 @@ async fn submit_turn( test: &TestCodex, prompt: &str, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, approvals_reviewer: Option, ) -> Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -149,7 +153,7 @@ async fn submit_turn( approval_policy, approvals_reviewer, sandbox_policy, - permission_profile: None, + permission_profile, model: session_model, effort: None, summary: None, @@ -199,14 +203,15 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s let server = start_mock_server().await; let approval_policy = AskForApproval::OnRequest; - let sandbox_policy = workspace_write_excluding_tmp(); - let sandbox_policy_for_config = sandbox_policy.clone(); + let permission_profile = workspace_write_excluding_tmp(); + let permission_profile_for_config = permission_profile.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); config - .set_legacy_sandbox_policy(sandbox_policy_for_config) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(permission_profile_for_config) + .expect("set permission profile"); config .features .enable(Feature::ExecPermissionApprovals) @@ -258,7 +263,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s &test, "write outside the workspace", approval_policy, - sandbox_policy, + permission_profile, /*approvals_reviewer*/ None, ) .await?; @@ -331,14 +336,15 @@ async fn approved_folder_write_request_permissions_unblocks_later_apply_patch() async fn apply_patch_after_request_permissions(strict_auto_review: bool) -> Result<()> { let server = start_mock_server().await; let approval_policy = AskForApproval::OnRequest; - let sandbox_policy = workspace_write_excluding_tmp(); - let sandbox_policy_for_config = sandbox_policy.clone(); + let permission_profile = workspace_write_excluding_tmp(); + let permission_profile_for_config = permission_profile.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); config - .set_legacy_sandbox_policy(sandbox_policy_for_config) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(permission_profile_for_config) + .expect("set permission profile"); config .features .enable(Feature::ExecPermissionApprovals) @@ -415,7 +421,7 @@ async fn apply_patch_after_request_permissions(strict_auto_review: bool) -> Resu &test, "patch outside the workspace", approval_policy, - sandbox_policy, + permission_profile, strict_auto_review.then_some(ApprovalsReviewer::User), ) .await?; 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, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index a2e15654e6..0947f4fba7 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -29,6 +29,7 @@ use codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY; 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::InputModality; use codex_protocol::openai_models::ModelInfo; @@ -41,7 +42,6 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpInvocation; use codex_protocol::protocol::McpToolCallBeginEvent; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use codex_utils_cargo_bin::cargo_bin; use core_test_support::assert_regex_match; @@ -53,6 +53,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_with_timeout; use reqwest::Client; @@ -92,6 +93,39 @@ fn assert_wall_time_header(output: &str) { assert_eq!(marker, "Output:"); } +fn read_only_user_turn(fixture: &TestCodex, text: impl Into) -> Op { + read_only_user_turn_with_model(fixture, text, fixture.session_configured.model.clone()) +} + +fn read_only_user_turn_with_model( + fixture: &TestCodex, + text: impl Into, + model: String, +) -> Op { + let cwd = fixture.cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd.as_path()); + Op::UserTurn { + items: vec![UserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd, + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + environments: None, + } +} + #[derive(Debug, PartialEq, Eq)] enum McpCallEvent { Begin(String), @@ -321,28 +355,9 @@ async fn call_cwd_tool( ) .await; - let session_model = fixture.session_configured.model.clone(); fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp cwd tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(fixture, "call the rmcp cwd tool")) .await?; wait_for_event(&fixture.codex, |ev| { @@ -455,29 +470,9 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .await?; let begin_event = wait_for_event(&fixture.codex, |ev| { @@ -797,9 +792,11 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> sleep(Duration::from_millis(200)).await; } - let sandbox_policy = SandboxPolicy::new_read_only_policy(); fixture - .submit_turn_with_policy("call the rmcp sandbox_meta tool", sandbox_policy.clone()) + .submit_turn_with_permission_profile( + "call the rmcp sandbox_meta tool", + PermissionProfile::read_only(), + ) .await?; let request = call_mock.single_request(); @@ -824,6 +821,8 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> let sandbox_meta = meta .get(MCP_SANDBOX_STATE_META_CAPABILITY) .expect("sandbox state metadata should be present"); + let (sandbox_policy, _) = + turn_permission_fields(PermissionProfile::read_only(), fixture.config.cwd.as_path()); let expected_sandbox_policy = serde_json::to_value(&sandbox_policy)?; assert_eq!( sandbox_meta.get("sandboxPolicy"), @@ -888,29 +887,12 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow:: }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp sync tool twice".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn( + &fixture, + "call the rmcp sync tool twice", + )) .await?; let mut call_events = Vec::new(); @@ -1022,29 +1004,12 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp sync tool twice".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn( + &fixture, + "call the rmcp sync tool twice", + )) .await?; wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1121,31 +1086,11 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - wait_for_mcp_tool(&fixture, &tool_name).await?; fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp image tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(&fixture, "call the rmcp image tool")) .await?; // Wait for tool begin/end and final completion. @@ -1274,31 +1219,14 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - wait_for_mcp_tool(&fixture, &tool_name).await?; fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp image_scenario tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn( + &fixture, + "call the rmcp image_scenario tool", + )) .await?; wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1435,25 +1363,11 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp image tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: text_only_model_slug.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn_with_model( + &fixture, + "call the rmcp image tool", + text_only_model_slug.to_string(), + )) .await?; wait_for_event(&fixture.codex, |ev| { @@ -1541,29 +1455,9 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .await?; let begin_event = wait_for_event(&fixture.codex, |ev| { @@ -1680,28 +1574,9 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .await?; wait_for_event(&fixture.codex, |ev| { @@ -1791,28 +1666,9 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .await?; wait_for_event(&fixture.codex, |ev| { @@ -1992,30 +1848,13 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - // Phase 4: submit the user turn that should trigger the MCP tool call. fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp streamable http echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn( + &fixture, + "call the rmcp streamable http echo tool", + )) .await?; // Phase 5: assert Codex begins the expected tool invocation. @@ -2196,8 +2035,6 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { }) .build_remote_aware(&server) .await?; - let session_model = fixture.session_configured.model.clone(); - // Phase 5: wait for MCP discovery to publish the expected tool before the // turn is submitted, which keeps failures tied to server startup/discovery. wait_for_mcp_tool(&fixture, &tool_name).await?; @@ -2205,25 +2042,10 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { // Phase 6: submit the user turn that should invoke the OAuth-backed tool. fixture .codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "call the rmcp streamable http oauth echo tool".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - environments: None, - }) + .submit(read_only_user_turn( + &fixture, + "call the rmcp streamable http oauth echo tool", + )) .await?; // Phase 7: assert Codex begins the expected tool invocation. diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index 2aa887a174..eb70dafe5f 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -1,5 +1,6 @@ use anyhow::Result; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CodexErrorInfo; @@ -7,7 +8,6 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ModelRerouteReason; use codex_protocol::protocol::ModelVerification; 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_function_call; @@ -20,7 +20,9 @@ use core_test_support::responses::sse_completed; use core_test_support::responses::sse_response; 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 pretty_assertions::assert_eq; use wiremock::ResponseTemplate; @@ -32,6 +34,30 @@ const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber"; const CYBER_POLICY_MESSAGE: &str = "This request has been flagged for potentially high-risk cyber activity."; +fn disabled_text_turn(test: &TestCodex, text: &str) -> Op { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); + Op::UserTurn { + environments: None, + items: vec![UserInput::Text { + text: text.to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model: REQUESTED_MODEL.to_string(), + effort: test.config.model_reasoning_effort, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> Result<()> { skip_if_no_network!(Ok(())); @@ -45,25 +71,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger safety check".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger safety check")) .await?; let reroute = wait_for_event(&test.codex, |event| { @@ -141,25 +149,7 @@ async fn cyber_policy_response_emits_typed_error_without_retry() -> Result<()> { let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger cyber policy error".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger cyber policy error")) .await?; let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; @@ -198,25 +188,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger response model check".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger response model check")) .await?; let reroute = wait_for_event(&test.codex, |event| { @@ -286,25 +258,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger follow-up turn".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger follow-up turn")) .await?; let mut warning_count = 0; @@ -338,25 +292,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger casing check".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger casing check")) .await?; let mut reroute_count = 0; @@ -399,25 +335,7 @@ async fn model_verification_emits_structured_event_without_reroute_or_warning() let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger model verification".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn(&test, "trigger model verification")) .await?; let mut verification_count = 0; @@ -493,25 +411,10 @@ async fn model_verification_only_emits_once_per_turn() -> Result<()> { let test = builder.build(&server).await?; test.codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { - text: "trigger follow-up model verification".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: REQUESTED_MODEL.to_string(), - effort: test.config.model_reasoning_effort, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + .submit(disabled_text_turn( + &test, + "trigger follow-up model verification", + )) .await?; let mut verification_count = 0; diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index 44c2fb7b68..72244d534e 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -2,21 +2,22 @@ #![cfg(unix)] use anyhow::Result; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::mount_function_call_agent_response; 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::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use core_test_support::zsh_fork::build_zsh_fork_test; -use core_test_support::zsh_fork::restrictive_workspace_write_policy; +use core_test_support::zsh_fork::restrictive_workspace_write_profile; use core_test_support::zsh_fork::zsh_fork_runtime; use std::fs; use std::path::Path; @@ -40,8 +41,10 @@ async fn submit_turn_with_policies( test: &TestCodex, prompt: &str, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, ) -> Result<()> { + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, test.cwd_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -54,7 +57,7 @@ async fn submit_turn_with_policies( approval_policy, approvals_reviewer: None, sandbox_policy, - permission_profile: None, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, @@ -144,7 +147,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<() request_permissions: true, mcp_elicitations: true, }); - let workspace_write_policy = restrictive_workspace_write_policy(); + let workspace_write_profile = restrictive_workspace_write_profile(); let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?; let allowed_dir = outside_dir.path().join("allowed-output"); fs::create_dir_all(&allowed_dir)?; @@ -165,7 +168,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<() &server, runtime, approval_policy, - workspace_write_policy.clone(), + workspace_write_profile.clone(), move |home| { let _ = fs::remove_file(&allowed_path_for_hook); write_skill_with_shell_script_contents( @@ -190,7 +193,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<() &test, "use $mbolin-test-skill", approval_policy, - workspace_write_policy, + workspace_write_profile, ) .await?; @@ -235,13 +238,13 @@ async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> { let server = start_mock_server().await; let tool_call_id = "zsh-fork-workspace-write-deny"; let outside_path = "/tmp/codex-zsh-fork-workspace-write-deny.txt"; - let workspace_write_policy = restrictive_workspace_write_policy(); + let workspace_write_profile = restrictive_workspace_write_profile(); let _ = fs::remove_file(outside_path); let test = build_zsh_fork_test( &server, runtime, AskForApproval::Never, - workspace_write_policy.clone(), + workspace_write_profile.clone(), move |_| { let _ = fs::remove_file(outside_path); }, @@ -258,7 +261,7 @@ async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> { &test, "write outside workspace with zsh fork", AskForApproval::Never, - workspace_write_policy, + workspace_write_profile, ) .await?; diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 46195e4dd0..b856074540 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -9,7 +9,6 @@ use std::time::Instant; use anyhow::Context; use anyhow::Result; -use codex_config::Constrained; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; use codex_core::sandboxing::SandboxPermissions; @@ -21,7 +20,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnEnvironmentSelection; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; @@ -547,14 +545,9 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> { skip_if_sandbox!(Ok(())); let server = start_mock_server().await; - let read_only_policy = SandboxPolicy::new_read_only_policy(); - let read_only_policy_for_config = read_only_policy.clone(); let mut builder = test_codex() .with_model("gpt-5.4") .with_config(move |config| { - config - .set_legacy_sandbox_policy(read_only_policy_for_config) - .expect("set sandbox policy"); let mut file_system_sandbox_policy = FileSystemSandboxPolicy::default(); file_system_sandbox_policy .entries @@ -564,11 +557,13 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> { }, access: FileSystemAccessMode::None, }); - config.permissions.permission_profile = - Constrained::allow_any(PermissionProfile::from_runtime_permissions( + config + .permissions + .set_permission_profile(PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, NetworkSandboxPolicy::Restricted, - )); + )) + .expect("set permission profile"); }); let fixture = builder.build(&server).await?; @@ -608,8 +603,9 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; + let permission_profile = fixture.session_configured.permission_profile.clone(); fixture - .submit_turn_with_policy("read the fixture files", read_only_policy) + .submit_turn_with_permission_profile("read the fixture files", permission_profile) .await?; let output_text = mock diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 972e500eee..9dd5d82e0a 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -6,6 +6,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_exec_server::CreateDirectoryOptions; use codex_login::CodexAuth; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; @@ -17,7 +18,6 @@ use codex_protocol::openai_models::TruncationPolicyConfig; 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; @@ -30,6 +30,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_with_timeout; use image::DynamicImage; use image::GenericImageView; @@ -50,6 +51,27 @@ use wiremock::matchers::body_string_contains; const VIEW_IMAGE_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); +fn disabled_user_turn(test: &TestCodex, items: Vec, model: String) -> Op { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); + Op::UserTurn { + environments: None, + items, + final_output_json_schema: None, + cwd: test.config.cwd.to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy, + permission_profile, + model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + } +} + fn image_messages(body: &Value) -> Vec<&Value> { body.get("input") .and_then(Value::as_array) @@ -137,7 +159,6 @@ async fn assert_user_turn_local_image_resizes_to( let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -158,24 +179,13 @@ async fn assert_user_turn_local_image_resizes_to( let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::LocalImage { + .submit(disabled_user_turn( + &test, + vec![UserInput::LocalImage { path: abs_path.clone(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -279,25 +289,14 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please add the screenshot".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; let mut tool_event = None; @@ -376,7 +375,6 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5 let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -412,25 +410,14 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5 let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please add the original screenshot".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -479,7 +466,6 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -513,25 +499,14 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please attach the image at low detail".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -569,7 +544,6 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -605,25 +579,14 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please attach the image with a null detail".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -671,7 +634,6 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -707,25 +669,14 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please add the screenshot".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -777,7 +728,6 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -813,25 +763,14 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please add the screenshot".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - service_tier: None, - summary: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -880,7 +819,6 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -907,25 +845,14 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please attach the folder".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -962,7 +889,6 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> { let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -990,25 +916,14 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> { let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please use the view_image tool to read the json file".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -1078,25 +993,14 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please attach the missing image".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout( @@ -1189,7 +1093,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an config.model = Some(model_slug.to_string()); }); let test = builder.build_remote_aware(&server).await?; - let TestCodex { codex, config, .. } = &test; + let TestCodex { codex, .. } = &test; let rel_path = "assets/example.png"; write_workspace_png( @@ -1217,25 +1121,14 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an let mock = responses::mount_sse_once(&server, second_response).await; codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::Text { + .submit(disabled_user_turn( + &test, + vec![UserInput::Text { text: "please attach the image".into(), text_elements: Vec::new(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: model_slug.to_string(), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + model_slug.to_string(), + )) .await?; wait_for_event_with_timeout( @@ -1289,7 +1182,6 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> let test = builder.build_remote_aware(&server).await?; let TestCodex { codex, - config, session_configured, .. } = &test; @@ -1300,24 +1192,13 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> let session_model = session_configured.model.clone(); codex - .submit(Op::UserTurn { - environments: None, - items: vec![UserInput::LocalImage { + .submit(disabled_user_turn( + &test, + vec![UserInput::LocalImage { path: abs_path.clone(), }], - final_output_json_schema: None, - cwd: config.cwd.to_path_buf(), - approval_policy: AskForApproval::Never, - approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, - model: session_model, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) + session_model, + )) .await?; wait_for_event_with_timeout(