diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index 6614a63bb7..f4c37da33a 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -15,14 +15,27 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::user_input::UserInput; use core_test_support::PathExt; -use core_test_support::load_default_config_for_test_with_cloud_config_bundle; +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_sequence; +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::wait_for_event; use pretty_assertions::assert_eq; +use serde_json::json; use serial_test::serial; use std::collections::HashMap; use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; use tempfile::TempDir; struct EnvVarGuard { @@ -328,23 +341,22 @@ async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyh #[serial(codex_home)] async fn windows_elevated_enforces_managed_deny_read_for_shell_subprocess() -> anyhow::Result<()> { stage_windows_sandbox_helpers()?; - let sandbox_home = - codex_home_for_windows_sandbox_test("windows-elevated-managed-deny-read-codex-home")?; - let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", sandbox_home.path().as_os_str()); - let config_home = TempDir::new()?; + let codex_home = Arc::new(TempDir::new()?); + let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); std::fs::write( - config_home.path().join(CONFIG_TOML_FILE), + codex_home.path().join(CONFIG_TOML_FILE), r#"[windows] sandbox = "elevated" "#, )?; - let workspace = TempDir::new()?; - let cwd = dunce::canonicalize(workspace.path())?.abs(); - let denied_path = cwd.join("secret.env"); - let allowed_path = cwd.join("public.txt"); + let protected_dir = TempDir::new()?; + let denied_path = protected_dir.path().join("secret.env"); + let allowed_path = protected_dir.path().join("public.txt"); std::fs::write(&denied_path, "managed secret\n")?; std::fs::write(&allowed_path, "public ok\n")?; + let denied_path = dunce::canonicalize(denied_path)?.abs(); + let allowed_path = dunce::canonicalize(allowed_path)?.abs(); let denied_path_toml = toml::Value::String(denied_path.to_string_lossy().into()).to_string(); let requirements = format!( r#"[permissions.filesystem] @@ -355,18 +367,26 @@ allowed_sandbox_implementations = ["elevated"] "# ); - let config = load_default_config_for_test_with_cloud_config_bundle( - &config_home, - CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements), - ) - .await; + let server = start_mock_server().await; + let mut builder = test_codex() + .with_home(Arc::clone(&codex_home)) + .with_cloud_config_bundle( + CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements), + ) + .with_windows_cmd_shell() + .with_config(|config| { + config.permissions.windows_sandbox_private_desktop = false; + }); + let fixture = builder.build(&server).await?; + assert_eq!( - config.permissions.windows_sandbox_mode, + fixture.config.permissions.windows_sandbox_mode, Some(codex_config::types::WindowsSandboxModeToml::Elevated) ); - let permission_profile = config.permissions.effective_permission_profile(); assert!( - permission_profile + fixture + .session_configured + .permission_profile .file_system_sandbox_policy() .entries .iter() @@ -377,58 +397,73 @@ allowed_sandbox_implementations = ["elevated"] FileSystemPath::Path { path } if path == &denied_path ) }), - "managed deny-read path should reach the runtime permission profile" + "managed deny-read path should reach the configured session" ); - let ExecToolCallOutput { - exit_code, - stdout, - .. - } = process_exec_tool_call( - ExecParams { - command: vec![ - "cmd.exe".to_string(), - "/D".to_string(), - "/C".to_string(), - "(type secret.env 1>NUL 2>NUL && echo SECRET-READ || echo SECRET-DENIED) & type public.txt" - .to_string(), - ], - cwd: cwd.clone(), - expiration: 10_000.into(), - capture_policy: ExecCapturePolicy::ShellTool, - env: HashMap::new(), - network: None, - sandbox_permissions: SandboxPermissions::UseDefault, - windows_sandbox_level: WindowsSandboxLevel::Elevated, - windows_sandbox_private_desktop: false, - justification: None, - arg0: None, - }, - &permission_profile, - &cwd, - std::slice::from_ref(&cwd), - &None, - /*use_legacy_landlock*/ false, - /*stdout_stream*/ None, + let call_id = "managed-deny-read"; + let command = format!( + "(type \"{}\" 1>NUL 2>NUL && echo SECRET-READ || echo SECRET-DENIED) & type \"{}\"", + denied_path.display(), + allowed_path.display() + ); + let args = json!({ + "command": command, + "login": false, + "timeout_ms": 10_000, + }); + let request_log = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_function_call(call_id, "shell_command", &serde_json::to_string(&args)?), + ev_completed("resp-1"), + ]), + sse(vec![ + ev_assistant_message("msg-1", "done"), + ev_completed("resp-2"), + ]), + ], ) - .await?; + .await; - assert_eq!(exit_code, 0, "sandboxed command should complete"); + fixture + .codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "read the fixture files".into(), + text_elements: Vec::new(), + }], + environments: None, + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + wait_for_event(&fixture.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let output = request_log + .function_call_output_text(call_id) + .context("shell output present")?; assert!( - stdout.text.contains("SECRET-DENIED"), - "managed deny-read should block the subprocess read: {stdout:?}" + output.contains("SECRET-DENIED"), + "managed deny-read should block the subprocess read: {output}" ); assert!( - !stdout.text.contains("SECRET-READ"), - "managed deny-read must not allow the subprocess read: {stdout:?}" + !output.contains("SECRET-READ"), + "managed deny-read must not allow the subprocess read: {output}" ); assert!( - stdout.text.contains("public ok"), - "allowed reads should still work: {stdout:?}" + output.contains("public ok"), + "allowed reads should still work: {output}" ); assert!( - !stdout.text.contains("managed secret"), - "denied file contents leaked into shell output: {stdout:?}" + !output.contains("managed secret"), + "denied file contents leaked into shell output: {output}" ); Ok(()) }