Download codex-rs/core/tests/suite/exec_policy.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 35.6 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/tests/suite/exec_policy.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core/tests/suite/exec_policy.rs
-
curl -L -o exec_policy.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/tests/suite/exec_policy.rs
35.6 kB
| use anyhow::Result; | |
| use codex_config::test_support::CloudConfigBundleFixture; | |
| use codex_core::EnvironmentConfig; | |
| use codex_core::TurnInputRequest; | |
| use codex_core::config::Constrained; | |
| use codex_core::windows_sandbox::WindowsSandboxLevelExt; | |
| use codex_execpolicy::Decision; | |
| use codex_execpolicy::Policy; | |
| use codex_execpolicy::RequirementsExecPolicy; | |
| use codex_features::Feature; | |
| use codex_protocol::config_types::ApprovalsReviewer; | |
| use codex_protocol::config_types::CollaborationMode; | |
| use codex_protocol::config_types::ModeKind; | |
| use codex_protocol::config_types::Settings; | |
| use codex_protocol::config_types::WindowsSandboxLevel; | |
| use codex_protocol::error::CodexErrorDetails; | |
| use codex_protocol::models::PermissionProfile; | |
| use codex_protocol::models::PermissionProfileSnapshot; | |
| use codex_protocol::protocol::AskForApproval; | |
| use codex_protocol::protocol::EventMsg; | |
| use codex_protocol::protocol::GranularApprovalConfig; | |
| use codex_protocol::protocol::Op; | |
| use codex_protocol::protocol::ReviewDecision; | |
| use codex_protocol::protocol::ThreadSettingsOverrides; | |
| use codex_protocol::user_input::UserInput; | |
| use core_test_support::responses::ev_assistant_message; | |
| use core_test_support::responses::ev_completed; | |
| use core_test_support::responses::ev_function_call; | |
| use core_test_support::responses::ev_response_created; | |
| 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::skip_if_target_windows; | |
| use core_test_support::skip_if_wine_exec; | |
| use core_test_support::test_codex::local_selections; | |
| use core_test_support::test_codex::test_codex; | |
| use core_test_support::test_codex::turn_permission_fields; | |
| use core_test_support::wait_for_event; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::Value; | |
| use serde_json::json; | |
| use std::fs; | |
| const COMPLEX_FORCED_RM_COMMAND: &str = "for target in \"\"; do rm -rf \"$target\"; done"; | |
| fn collaboration_mode_for_model(model: String) -> CollaborationMode { | |
| CollaborationMode { | |
| mode: ModeKind::Default, | |
| settings: Settings { | |
| model, | |
| reasoning_effort: None, | |
| developer_instructions: Some("exercise approvals in collaboration mode".to_string()), | |
| }, | |
| } | |
| } | |
| async fn submit_user_turn( | |
| test: &core_test_support::test_codex::TestCodex, | |
| prompt: &str, | |
| approval_policy: AskForApproval, | |
| permission_profile: PermissionProfile, | |
| collaboration_mode: Option<CollaborationMode>, | |
| ) -> 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 | |
| .start_or_steer_turn( | |
| TurnInputRequest::user_input(vec![UserInput::Text { | |
| text: prompt.into(), | |
| text_elements: Vec::new(), | |
| }]) | |
| .with_thread_settings(ThreadSettingsOverrides { | |
| environments: Some(local_selections(test.config.cwd.clone())), | |
| approval_policy: Some(approval_policy), | |
| sandbox_policy: Some(sandbox_policy), | |
| permission_profile, | |
| collaboration_mode: collaboration_mode.or({ | |
| Some(CollaborationMode { | |
| mode: ModeKind::Default, | |
| settings: Settings { | |
| model: session_model, | |
| reasoning_effort: None, | |
| developer_instructions: None, | |
| }, | |
| }) | |
| }), | |
| ..Default::default() | |
| }), | |
| ) | |
| .await?; | |
| Ok(()) | |
| } | |
| fn assert_no_matched_rules_invariant(output_item: &Value) { | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| !output.contains("invariant failed: matched_rules must be non-empty"), | |
| "unexpected invariant panic surfaced in output: {output}" | |
| ); | |
| } | |
| async fn git_status_requires_approval_under_unless_trusted() -> Result<()> { | |
| skip_if_wine_exec!(Ok(()), "command approval requires host-native paths"); | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); | |
| config | |
| .permissions | |
| .set_permission_profile(PermissionProfile::workspace_write()) | |
| .expect("set workspace-write permissions"); | |
| config.approvals_reviewer = ApprovalsReviewer::User; | |
| }); | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let call_id = "git-status-approval"; | |
| let args = json!({"cmd": "git status", "yield_time_ms": 1_000}); | |
| let initial_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-git-status-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-git-status-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-git-status-1", "done"), | |
| ev_completed("resp-git-status-2"), | |
| ]), | |
| ) | |
| .await; | |
| test.codex | |
| .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { | |
| text: "check git status".into(), | |
| text_elements: Vec::new(), | |
| }])) | |
| .await?; | |
| let event = wait_for_event(&test.codex, |event| { | |
| matches!( | |
| event, | |
| EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) | |
| ) | |
| }) | |
| .await; | |
| let EventMsg::ExecApprovalRequest(approval) = event else { | |
| let output = results_mock.function_call_output_text(call_id); | |
| panic!( | |
| "expected git status to request approval before turn completion; output: {output:?}" | |
| ); | |
| }; | |
| assert_eq!(approval.call_id, call_id); | |
| test.codex | |
| .submit(Op::ExecApproval { | |
| id: approval.effective_approval_id(), | |
| turn_id: None, | |
| decision: ReviewDecision::denied("git status was not approved"), | |
| }) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| assert!( | |
| initial_mock | |
| .single_request() | |
| .message_input_texts("user") | |
| .iter() | |
| .any(|text| text == "check git status") | |
| ); | |
| let output = results_mock | |
| .single_request() | |
| .function_call_output_text(call_id) | |
| .expect("shell command output"); | |
| assert!(output.contains("git status was not approved"), "{output}"); | |
| Ok(()) | |
| } | |
| async fn startup_migrates_default_policy_and_honors_ignore_rules() -> Result<()> { | |
| const LEGACY_POLICY: &str = r#"prefix_rule(pattern=["rm"], decision="allow") | |
| prefix_rule(pattern=["git", "status"], decision="allow") | |
| "#; | |
| const MIGRATED_POLICY: &str = r#"prefix_rule(pattern=["git", "status"], decision="allow") | |
| "#; | |
| const MIGRATION_MARKER_FILENAME: &str = ".sandbox_migration"; | |
| let server = start_mock_server().await; | |
| let mut migrated_builder = test_codex().with_config(|config| { | |
| let policy_path = config.codex_home.join("rules/default.rules"); | |
| fs::create_dir_all(policy_path.parent().expect("rules directory")) | |
| .expect("create rules directory"); | |
| fs::write(policy_path, LEGACY_POLICY).expect("write legacy policy"); | |
| }); | |
| let migrated = migrated_builder.build_with_auto_env(&server).await?; | |
| let migrated_policy_path = migrated.codex_home_path().join("rules/default.rules"); | |
| assert_eq!(fs::read_to_string(&migrated_policy_path)?, MIGRATED_POLICY); | |
| assert_eq!( | |
| fs::read_to_string(migrated.codex_home_path().join(MIGRATION_MARKER_FILENAME))?, | |
| "v1\n" | |
| ); | |
| let mut ignored_builder = test_codex().with_config(|config| { | |
| let policy_path = config.codex_home.join("rules/default.rules"); | |
| fs::create_dir_all(policy_path.parent().expect("rules directory")) | |
| .expect("create rules directory"); | |
| fs::write(policy_path, LEGACY_POLICY).expect("write legacy policy"); | |
| config.config_layer_stack = config | |
| .config_layer_stack | |
| .clone() | |
| .with_user_and_project_exec_policy_rules_ignored( | |
| /*ignore_user_and_project_exec_policy_rules*/ true, | |
| ); | |
| }); | |
| let ignored = ignored_builder.build_with_auto_env(&server).await?; | |
| let ignored_policy_path = ignored.codex_home_path().join("rules/default.rules"); | |
| assert_eq!(fs::read_to_string(&ignored_policy_path)?, LEGACY_POLICY); | |
| assert!( | |
| !ignored | |
| .codex_home_path() | |
| .join(MIGRATION_MARKER_FILENAME) | |
| .exists() | |
| ); | |
| Ok(()) | |
| } | |
| async fn granular_complex_forced_rm_denial_explains_why_the_command_was_rejected() -> Result<()> { | |
| skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex(); | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let call_id = "forced-rm-denied"; | |
| let args = json!({ | |
| "cmd": COMPLEX_FORCED_RM_COMMAND, | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-forced-rm-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-forced-rm-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-forced-rm-1", "done"), | |
| ev_completed("resp-forced-rm-2"), | |
| ]), | |
| ) | |
| .await; | |
| submit_user_turn( | |
| &test, | |
| "run the forced rm loop", | |
| AskForApproval::Granular(GranularApprovalConfig { | |
| sandbox_approval: false, | |
| rules: true, | |
| skill_approval: true, | |
| request_permissions: true, | |
| mcp_elicitations: true, | |
| }), | |
| PermissionProfile::read_only(), | |
| /*collaboration_mode*/ None, | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| output.contains("rm -f style commands are not permitted. Use a safer approach"), | |
| "unexpected output: {output}" | |
| ); | |
| Ok(()) | |
| } | |
| async fn granular_complex_forced_rm_requests_approval_when_allowed() -> Result<()> { | |
| skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex(); | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let call_id = "forced-rm-approval"; | |
| let args = json!({ | |
| "cmd": COMPLEX_FORCED_RM_COMMAND, | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-forced-rm-approval-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-forced-rm-approval-1"), | |
| ]), | |
| ) | |
| .await; | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-forced-rm-approval-1", "done"), | |
| ev_completed("resp-forced-rm-approval-2"), | |
| ]), | |
| ) | |
| .await; | |
| submit_user_turn( | |
| &test, | |
| "run the forced rm loop", | |
| AskForApproval::Granular(GranularApprovalConfig { | |
| sandbox_approval: true, | |
| rules: true, | |
| skill_approval: true, | |
| request_permissions: true, | |
| mcp_elicitations: true, | |
| }), | |
| PermissionProfile::read_only(), | |
| /*collaboration_mode*/ None, | |
| ) | |
| .await?; | |
| let approval_event = wait_for_event(&test.codex, |event| { | |
| matches!( | |
| event, | |
| EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) | |
| ) | |
| }) | |
| .await; | |
| let EventMsg::ExecApprovalRequest(approval) = approval_event else { | |
| panic!("expected forced rm to request approval before turn completion"); | |
| }; | |
| assert_eq!( | |
| approval.command.last().map(String::as_str), | |
| Some(COMPLEX_FORCED_RM_COMMAND) | |
| ); | |
| test.codex | |
| .submit(Op::ExecApproval { | |
| id: approval.effective_approval_id(), | |
| turn_id: None, | |
| decision: ReviewDecision::denied("rejected by user"), | |
| }) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| Ok(()) | |
| } | |
| async fn deeply_nested_forced_rm_is_rejected_before_execution_when_approvals_are_disabled() | |
| -> Result<()> { | |
| skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex(); | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let sentinel = test.config.cwd.join("forced-rm-sentinel"); | |
| fs::write(&sentinel, "must not be deleted")?; | |
| let call_id = "deeply-nested-forced-rm"; | |
| let args = json!({ | |
| "cmd": "env env env env env env env env env rm -rf forced-rm-sentinel", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-deeply-nested-forced-rm-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-deeply-nested-forced-rm-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-deeply-nested-forced-rm", "done"), | |
| ev_completed("resp-deeply-nested-forced-rm-2"), | |
| ]), | |
| ) | |
| .await; | |
| submit_user_turn( | |
| &test, | |
| "run the deeply nested forced rm", | |
| AskForApproval::Never, | |
| PermissionProfile::Disabled, | |
| /*collaboration_mode*/ None, | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| output.contains("rejected: blocked by policy"), | |
| "unexpected output: {output}" | |
| ); | |
| assert!(sentinel.exists(), "the rejected command must not execute"); | |
| Ok(()) | |
| } | |
| async fn unified_exec_disabled_windows_sandbox_rejects_managed_read_only_command() -> Result<()> { | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex().with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| config | |
| .features | |
| .disable(Feature::WindowsSandbox) | |
| .expect("test config should allow feature update"); | |
| config | |
| .features | |
| .disable(Feature::WindowsSandboxElevated) | |
| .expect("test config should allow feature update"); | |
| config.set_windows_sandbox_enabled(false); | |
| config.set_windows_elevated_sandbox_enabled(false); | |
| }); | |
| let test = builder.build(&server).await?; | |
| let call_id = "unified-exec-disabled-windows-sandbox-read-only"; | |
| let args = json!({ | |
| "cmd": "cmd.exe /c dir", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-disabled-windows-sandbox-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-disabled-windows-sandbox-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-disabled-windows-sandbox-1", "done"), | |
| ev_completed("resp-disabled-windows-sandbox-2"), | |
| ]), | |
| ) | |
| .await; | |
| submit_user_turn( | |
| &test, | |
| "run unified exec with disabled Windows sandbox", | |
| AskForApproval::Never, | |
| PermissionProfile::read_only(), | |
| None, | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| output.contains("cmd.exe /c dir") && output.contains("rejected: blocked by policy"), | |
| "unexpected output: {output}", | |
| ); | |
| Ok(()) | |
| } | |
| async fn execpolicy_blocks_shell_invocation() -> Result<()> { | |
| let mut builder = test_codex().with_config(|config| { | |
| let policy_path = config.codex_home.join("rules").join("policy.rules"); | |
| fs::create_dir_all( | |
| policy_path | |
| .parent() | |
| .expect("policy directory must have a parent"), | |
| ) | |
| .expect("create policy directory"); | |
| fs::write( | |
| &policy_path, | |
| r#"prefix_rule(pattern=["echo"], decision="forbidden")"#, | |
| ) | |
| .expect("write policy file"); | |
| }); | |
| let server = start_mock_server().await; | |
| let test = builder.build(&server).await?; | |
| let call_id = "shell-forbidden"; | |
| let args = json!({ | |
| "cmd": "echo blocked", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-1", "done"), | |
| ev_completed("resp-2"), | |
| ]), | |
| ) | |
| .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 | |
| .start_or_steer_turn( | |
| TurnInputRequest::user_input(vec![UserInput::Text { | |
| text: "run shell command".into(), | |
| text_elements: Vec::new(), | |
| }]) | |
| .with_thread_settings(ThreadSettingsOverrides { | |
| environments: Some(local_selections(test.config.cwd.clone())), | |
| approval_policy: Some(AskForApproval::Never), | |
| sandbox_policy: Some(sandbox_policy), | |
| permission_profile, | |
| collaboration_mode: Some(CollaborationMode { | |
| mode: ModeKind::Default, | |
| settings: Settings { | |
| model: session_model, | |
| reasoning_effort: None, | |
| developer_instructions: None, | |
| }, | |
| }), | |
| ..Default::default() | |
| }), | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output = results_mock | |
| .single_request() | |
| .function_call_output_text(call_id) | |
| .expect("forbidden command should produce a tool response"); | |
| assert!( | |
| output.contains("policy forbids commands starting with `echo`"), | |
| "unexpected output: {output}" | |
| ); | |
| Ok(()) | |
| } | |
| async fn malformed_custom_rules_preserve_managed_forbidden_prefix() -> Result<()> { | |
| skip_if_target_windows!( | |
| Ok(()), | |
| "managed prefix fixture uses POSIX executable semantics" | |
| ); | |
| let mut builder = test_codex() | |
| .with_cloud_config_bundle( | |
| CloudConfigBundleFixture::loader_with_enterprise_requirement( | |
| r#" | |
| [rules] | |
| prefix_rules = [ | |
| { pattern = [{ token = "echo" }], decision = "forbidden" }, | |
| ] | |
| "#, | |
| ), | |
| ) | |
| .with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| let policy_path = config.codex_home.join("rules").join("broken.rules"); | |
| fs::create_dir_all( | |
| policy_path | |
| .parent() | |
| .expect("policy directory must have a parent"), | |
| ) | |
| .expect("create policy directory"); | |
| fs::write(policy_path, "prefix_rule(").expect("write malformed policy file"); | |
| }); | |
| let server = start_mock_server().await; | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let call_id = "managed-shell-forbidden"; | |
| let args = json!({ | |
| "cmd": "echo blocked", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-managed-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-managed-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-managed-1", "done"), | |
| ev_completed("resp-managed-2"), | |
| ]), | |
| ) | |
| .await; | |
| test.submit_turn_with_approval_and_permission_profile( | |
| "run shell command", | |
| AskForApproval::Never, | |
| PermissionProfile::Disabled, | |
| ) | |
| .await?; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| output.contains("policy forbids commands starting with `echo`"), | |
| "unexpected output: {output}" | |
| ); | |
| Ok(()) | |
| } | |
| async fn environment_command_restrictions_override_saved_prefix_approvals() -> Result<()> { | |
| skip_if_target_windows!( | |
| Ok(()), | |
| "managed prefix fixture uses POSIX executable semantics" | |
| ); | |
| let mut builder = test_codex().with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| let policy_path = config.codex_home.join("rules").join("approved.rules"); | |
| fs::create_dir_all(policy_path.parent().expect("rules directory")) | |
| .expect("create rules directory"); | |
| fs::write( | |
| policy_path, | |
| r#"prefix_rule(pattern=["echo"], decision="allow")"#, | |
| ) | |
| .expect("write approved command prefix"); | |
| }); | |
| let server = start_mock_server().await; | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let selection = test | |
| .codex | |
| .environment_selections() | |
| .await | |
| .into_iter() | |
| .next() | |
| .expect("thread should select its executor environment"); | |
| let mut invalid_policy = Policy::empty(); | |
| invalid_policy.add_prefix_rule(&["echo".to_string()], Decision::Allow)?; | |
| let error = test | |
| .codex | |
| .environment_ready( | |
| &selection, | |
| EnvironmentConfig { | |
| allow_login_shell: true, | |
| workspace_roots: selection.workspace_roots.clone(), | |
| permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), | |
| shell_environment_policy: Default::default(), | |
| windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), | |
| windows_sandbox_private_desktop: test | |
| .config | |
| .permissions | |
| .windows_sandbox_private_desktop, | |
| use_legacy_landlock: test.config.features.use_legacy_landlock(), | |
| exec_policy: Some(RequirementsExecPolicy::new(invalid_policy)), | |
| mcp_policy: None, | |
| network_policy: None, | |
| selected_capability_roots: Vec::new(), | |
| }, | |
| ) | |
| .await | |
| .expect_err("environment policies must not introduce command allowances"); | |
| assert!(matches!( | |
| error.details(), | |
| CodexErrorDetails::InvalidRequest(message) | |
| if message == "environment command policy cannot contain allow rules" | |
| )); | |
| let mut environment_policy = Policy::empty(); | |
| environment_policy.add_prefix_rule(&["echo".to_string()], Decision::Forbidden)?; | |
| test.codex | |
| .environment_ready( | |
| &selection, | |
| EnvironmentConfig { | |
| allow_login_shell: true, | |
| workspace_roots: selection.workspace_roots.clone(), | |
| permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), | |
| shell_environment_policy: Default::default(), | |
| windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), | |
| windows_sandbox_private_desktop: test | |
| .config | |
| .permissions | |
| .windows_sandbox_private_desktop, | |
| use_legacy_landlock: test.config.features.use_legacy_landlock(), | |
| exec_policy: Some(RequirementsExecPolicy::new(environment_policy)), | |
| mcp_policy: None, | |
| network_policy: None, | |
| selected_capability_roots: Vec::new(), | |
| }, | |
| ) | |
| .await?; | |
| let call_id = "environment-managed-command"; | |
| let args = json!({ | |
| "cmd": "echo blocked", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-environment-managed-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-environment-managed-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-environment-managed", "done"), | |
| ev_completed("resp-environment-managed-2"), | |
| ]), | |
| ) | |
| .await; | |
| test.submit_turn_with_approval_and_permission_profile( | |
| "run shell command", | |
| AskForApproval::Never, | |
| PermissionProfile::Disabled, | |
| ) | |
| .await?; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| let output = output_item | |
| .get("output") | |
| .and_then(Value::as_str) | |
| .expect("function call output should include a string output payload"); | |
| assert!( | |
| output.contains("policy forbids commands starting with `echo`"), | |
| "unexpected output: {output}" | |
| ); | |
| Ok(()) | |
| } | |
| async fn environment_command_policy_changes_invalidate_session_approvals() -> Result<()> { | |
| skip_if_target_windows!( | |
| Ok(()), | |
| "managed prefix fixture uses POSIX executable semantics" | |
| ); | |
| let mut builder = test_codex().with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); | |
| config | |
| .permissions | |
| .set_permission_profile(PermissionProfile::Disabled) | |
| .expect("test config should allow unrestricted permissions"); | |
| config.approvals_reviewer = ApprovalsReviewer::User; | |
| let policy_path = config.codex_home.join("rules").join("prompt.rules"); | |
| fs::create_dir_all(policy_path.parent().expect("rules directory")) | |
| .expect("create rules directory"); | |
| fs::write( | |
| policy_path, | |
| r#"prefix_rule(pattern=["echo"], decision="prompt")"#, | |
| ) | |
| .expect("write prompt command prefix"); | |
| }); | |
| let server = start_mock_server().await; | |
| let test = builder.build_with_auto_env(&server).await?; | |
| let selection = test | |
| .codex | |
| .environment_selections() | |
| .await | |
| .into_iter() | |
| .next() | |
| .expect("thread should select its executor environment"); | |
| for (attempt, decision) in [ | |
| ("before-owner-policy", ReviewDecision::ApprovedForSession), | |
| ("after-owner-policy", ReviewDecision::Approved), | |
| ] { | |
| if attempt == "after-owner-policy" { | |
| let mut policy = Policy::empty(); | |
| policy.add_prefix_rule(&["echo".to_string()], Decision::Prompt)?; | |
| test.codex | |
| .environment_ready( | |
| &selection, | |
| EnvironmentConfig { | |
| allow_login_shell: true, | |
| workspace_roots: selection.workspace_roots.clone(), | |
| permission_profile: PermissionProfileSnapshot::legacy( | |
| PermissionProfile::Disabled, | |
| ), | |
| shell_environment_policy: Default::default(), | |
| windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), | |
| windows_sandbox_private_desktop: test | |
| .config | |
| .permissions | |
| .windows_sandbox_private_desktop, | |
| use_legacy_landlock: test.config.features.use_legacy_landlock(), | |
| exec_policy: Some(RequirementsExecPolicy::new(policy)), | |
| mcp_policy: None, | |
| network_policy: None, | |
| selected_capability_roots: Vec::new(), | |
| }, | |
| ) | |
| .await?; | |
| } | |
| let args = json!({ "cmd": "echo approval", "yield_time_ms": 1_000 }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created(attempt), | |
| ev_function_call(attempt, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed(attempt), | |
| ]), | |
| ) | |
| .await; | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message(attempt, "done"), | |
| ev_completed(attempt), | |
| ]), | |
| ) | |
| .await; | |
| test.codex | |
| .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { | |
| text: attempt.into(), | |
| text_elements: Vec::new(), | |
| }])) | |
| .await?; | |
| let event = wait_for_event(&test.codex, |event| { | |
| matches!( | |
| event, | |
| EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) | |
| ) | |
| }) | |
| .await; | |
| let EventMsg::ExecApprovalRequest(approval) = event else { | |
| panic!("expected a fresh command approval {attempt}"); | |
| }; | |
| test.codex | |
| .submit(Op::ExecApproval { | |
| id: approval.effective_approval_id(), | |
| turn_id: None, | |
| decision, | |
| }) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| } | |
| Ok(()) | |
| } | |
| async fn unified_exec_empty_script_with_collaboration_mode_does_not_panic() -> Result<()> { | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| config | |
| .features | |
| .enable(Feature::CollaborationModes) | |
| .expect("test config should allow feature update"); | |
| }); | |
| let test = builder.build(&server).await?; | |
| let call_id = "unified-exec-empty-script-collab"; | |
| let args = json!({ | |
| "cmd": "", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-empty-unified-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-empty-unified-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-empty-unified-1", "done"), | |
| ev_completed("resp-empty-unified-2"), | |
| ]), | |
| ) | |
| .await; | |
| let collaboration_mode = collaboration_mode_for_model(test.session_configured.model.clone()); | |
| submit_user_turn( | |
| &test, | |
| "run empty unified exec command", | |
| AskForApproval::OnRequest, | |
| PermissionProfile::Disabled, | |
| Some(collaboration_mode), | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| assert_no_matched_rules_invariant(&output_item); | |
| Ok(()) | |
| } | |
| async fn unified_exec_whitespace_script_with_collaboration_mode_does_not_panic() -> Result<()> { | |
| let server = start_mock_server().await; | |
| let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { | |
| config | |
| .features | |
| .enable(Feature::UnifiedExec) | |
| .expect("test config should allow feature update"); | |
| config | |
| .features | |
| .enable(Feature::CollaborationModes) | |
| .expect("test config should allow feature update"); | |
| }); | |
| let test = builder.build(&server).await?; | |
| let call_id = "unified-exec-whitespace-script-collab"; | |
| let args = json!({ | |
| "cmd": " \n \t", | |
| "yield_time_ms": 1_000, | |
| }); | |
| mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_response_created("resp-whitespace-unified-1"), | |
| ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), | |
| ev_completed("resp-whitespace-unified-1"), | |
| ]), | |
| ) | |
| .await; | |
| let results_mock = mount_sse_once( | |
| &server, | |
| sse(vec![ | |
| ev_assistant_message("msg-whitespace-unified-1", "done"), | |
| ev_completed("resp-whitespace-unified-2"), | |
| ]), | |
| ) | |
| .await; | |
| let collaboration_mode = collaboration_mode_for_model(test.session_configured.model.clone()); | |
| submit_user_turn( | |
| &test, | |
| "run whitespace unified exec command", | |
| AskForApproval::OnRequest, | |
| PermissionProfile::Disabled, | |
| Some(collaboration_mode), | |
| ) | |
| .await?; | |
| wait_for_event(&test.codex, |event| { | |
| matches!(event, EventMsg::TurnComplete(_)) | |
| }) | |
| .await; | |
| let output_item = results_mock.single_request().function_call_output(call_id); | |
| assert_no_matched_rules_invariant(&output_item); | |
| Ok(()) | |
| } | |