From d6fe246e87669f66c7d172c7e8bcd68745e34be9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 01/20] tui: use permission profiles for sandbox state --- codex-rs/tui/src/additional_dirs.rs | 106 ++++++-- codex-rs/tui/src/app.rs | 44 ++-- codex-rs/tui/src/app/config_persistence.rs | 53 ++-- codex-rs/tui/src/app/event_dispatch.rs | 86 ++++--- codex-rs/tui/src/app/platform_actions.rs | 12 +- codex-rs/tui/src/app/test_support.rs | 2 +- codex-rs/tui/src/app/tests.rs | 47 ++-- codex-rs/tui/src/app/thread_routing.rs | 2 - codex-rs/tui/src/app/thread_session_state.rs | 21 +- codex-rs/tui/src/app_command.rs | 45 ++-- codex-rs/tui/src/app_event.rs | 6 +- codex-rs/tui/src/app_server_session.rs | 231 ++++++++---------- codex-rs/tui/src/chatwidget.rs | 175 ++++++------- .../src/chatwidget/tests/history_replay.rs | 37 ++- .../tui/src/chatwidget/tests/permissions.rs | 104 ++++++-- codex-rs/tui/src/lib.rs | 15 +- codex-rs/tui/src/status/card.rs | 52 ++-- ...ched_limits_hide_credits_without_flag.snap | 34 +-- ..._snapshot_includes_credits_and_limits.snap | 34 +-- ..._status_snapshot_includes_forked_from.snap | 34 +-- ...tatus_snapshot_includes_monthly_limit.snap | 30 +-- ...snapshot_shows_missing_limits_message.snap | 30 +-- ...apshot_shows_refreshing_limits_notice.snap | 32 +-- ...s_snapshot_shows_stale_limits_message.snap | 34 +-- ...shot_shows_unavailable_limits_message.snap | 30 +-- ...efreshing_empty_limits_as_unavailable.snap | 30 +-- ...snapshot_truncates_in_narrow_terminal.snap | 2 +- ...s_default_reasoning_when_config_empty.snap | 30 +-- codex-rs/tui/src/status/tests.rs | 161 ++++++++---- codex-rs/utils/approval-presets/src/lib.rs | 16 +- codex-rs/utils/sandbox-summary/src/lib.rs | 1 + .../sandbox-summary/src/sandbox_summary.rs | 15 ++ 32 files changed, 874 insertions(+), 677 deletions(-) diff --git a/codex-rs/tui/src/additional_dirs.rs b/codex-rs/tui/src/additional_dirs.rs index f7d2ef5508..4bf66a3206 100644 --- a/codex-rs/tui/src/additional_dirs.rs +++ b/codex-rs/tui/src/additional_dirs.rs @@ -1,23 +1,35 @@ -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use std::path::PathBuf; /// Returns a warning describing why `--add-dir` entries will be ignored for the -/// resolved sandbox policy. The caller is responsible for presenting the +/// resolved permission profile. The caller is responsible for presenting the /// warning to the user (for example, printing to stderr). pub fn add_dir_warning_message( additional_dirs: &[PathBuf], - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, + cwd: &std::path::Path, ) -> Option { if additional_dirs.is_empty() { return None; } - match sandbox_policy { - SandboxPolicy::WorkspaceWrite { .. } - | SandboxPolicy::DangerFullAccess - | SandboxPolicy::ExternalSandbox { .. } => None, - SandboxPolicy::ReadOnly { .. } => Some(format_warning(additional_dirs)), + if matches!( + permission_profile, + PermissionProfile::Disabled | PermissionProfile::External { .. } + ) { + return None; } + + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + return None; + } + + if file_system_policy.can_write_path_with_cwd(cwd, cwd) { + return None; + } + + Some(format_warning(additional_dirs)) } fn format_warning(additional_dirs: &[PathBuf]) -> String { @@ -27,57 +39,105 @@ fn format_warning(additional_dirs: &[PathBuf]) -> String { .collect::>() .join(", "); format!( - "Ignoring --add-dir ({joined_paths}) because the effective sandbox mode is read-only. Switch to workspace-write or danger-full-access to allow additional writable roots." + "Ignoring --add-dir ({joined_paths}) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them." ) } #[cfg(test)] mod tests { use super::add_dir_warning_message; - use codex_protocol::protocol::NetworkAccess; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::ManagedFileSystemPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; + use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; + use std::path::Path; use std::path::PathBuf; #[test] fn returns_none_for_workspace_write() { - let sandbox = SandboxPolicy::new_workspace_write_policy(); + let profile = PermissionProfile::workspace_write(); let dirs = vec![PathBuf::from("/tmp/example")]; - assert_eq!(add_dir_warning_message(&dirs, &sandbox), None); + assert_eq!( + add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")), + None + ); } #[test] fn returns_none_for_danger_full_access() { - let sandbox = SandboxPolicy::DangerFullAccess; + let profile = PermissionProfile::Disabled; let dirs = vec![PathBuf::from("/tmp/example")]; - assert_eq!(add_dir_warning_message(&dirs, &sandbox), None); + assert_eq!( + add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")), + None + ); } #[test] fn returns_none_for_external_sandbox() { - let sandbox = SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Enabled, + let profile = PermissionProfile::External { + network: NetworkSandboxPolicy::Enabled, }; let dirs = vec![PathBuf::from("/tmp/example")]; - assert_eq!(add_dir_warning_message(&dirs, &sandbox), None); + assert_eq!( + add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")), + None + ); } #[test] fn warns_for_read_only() { - let sandbox = SandboxPolicy::new_read_only_policy(); + let profile = PermissionProfile::read_only(); let dirs = vec![PathBuf::from("relative"), PathBuf::from("/abs")]; - let message = add_dir_warning_message(&dirs, &sandbox) + let message = add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")) .expect("expected warning for read-only sandbox"); assert_eq!( message, - "Ignoring --add-dir (relative, /abs) because the effective sandbox mode is read-only. Switch to workspace-write or danger-full-access to allow additional writable roots." + "Ignoring --add-dir (relative, /abs) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them." + ); + } + + #[test] + fn warns_when_profile_can_write_elsewhere_but_not_cwd() { + let profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: "/tmp/writable".try_into().expect("absolute path"), + }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + let dirs = vec![PathBuf::from("/tmp/extra")]; + + assert_eq!( + add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")), + Some("Ignoring --add-dir (/tmp/extra) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them.".to_string()) ); } #[test] fn returns_none_when_no_additional_dirs() { - let sandbox = SandboxPolicy::new_read_only_policy(); + let profile = PermissionProfile::read_only(); let dirs: Vec = Vec::new(); - assert_eq!(add_dir_warning_message(&dirs, &sandbox), None); + assert_eq!( + add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")), + None + ); } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e454c83c5b..1d138f9da4 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -131,11 +131,14 @@ use codex_protocol::approvals::ExecApprovalRequestEvent; use codex_protocol::config_types::Personality; #[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelAvailabilityNux; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::AskForApproval; +#[cfg(target_os = "windows")] +use codex_protocol::protocol::FileSystemSandboxKind; use codex_protocol::protocol::FinalOutput; use codex_protocol::protocol::GetHistoryEntryResponseEvent; use codex_protocol::protocol::ListSkillsResponseEvent; @@ -143,7 +146,6 @@ use codex_protocol::protocol::ListSkillsResponseEvent; use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::Op; use codex_protocol::protocol::RateLimitSnapshot; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillErrorInfo; use codex_protocol::protocol::TokenUsage; @@ -308,7 +310,7 @@ fn default_exec_approval_decisions( struct AutoReviewMode { approval_policy: AskForApproval, approvals_reviewer: ApprovalsReviewer, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, } /// Enabling the Auto-review experiment in the TUI should also switch the @@ -319,9 +321,18 @@ fn auto_review_mode() -> AutoReviewMode { AutoReviewMode { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::workspace_write(), } } + +#[cfg(target_os = "windows")] +fn managed_filesystem_sandbox_is_restricted(permission_profile: &PermissionProfile) -> bool { + matches!( + permission_profile.file_system_sandbox_policy().kind, + FileSystemSandboxKind::Restricted + ) +} + /// Baseline cadence for periodic stream commit animation ticks. /// /// Smooth-mode streaming drains one line per tick, so this interval controls @@ -508,7 +519,7 @@ pub(crate) struct App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, runtime_approval_policy_override: Option, - runtime_sandbox_policy_override: Option, + runtime_permission_profile_override: Option, pub(crate) file_search: FileSearchManager, @@ -906,7 +917,7 @@ See the Codex keymap documentation for supported actions and examples." cli_kv_overrides, harness_overrides, runtime_approval_policy_override: None, - runtime_sandbox_policy_override: None, + runtime_permission_profile_override: None, file_search, enhanced_keys_supported, keymap: runtime_keymap, @@ -947,20 +958,14 @@ See the Codex keymap documentation for supported actions and examples." .await?; } - // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. + // On startup, if a managed filesystem sandbox is active, warn about + // world-writable dirs on Windows. #[cfg(target_os = "windows")] { - let startup_sandbox_policy = app - .config - .permissions - .legacy_sandbox_policy(app.config.cwd.as_path()); + let startup_permission_profile = app.config.permissions.permission_profile(); let should_check = WindowsSandboxLevel::from_config(&app.config) != WindowsSandboxLevel::Disabled - && matches!( - &startup_sandbox_policy, - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } - ) + && managed_filesystem_sandbox_is_restricted(&startup_permission_profile) && !app .config .notices @@ -971,8 +976,13 @@ See the Codex keymap documentation for supported actions and examples." let env_map: std::collections::HashMap = std::env::vars().collect(); let tx = app.app_event_tx.clone(); let logs_base_dir = app.config.codex_home.clone(); - let sandbox_policy = startup_sandbox_policy; - Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx); + Self::spawn_world_writable_scan( + cwd, + env_map, + logs_base_dir, + startup_permission_profile, + tx, + ); } } diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 3164655374..3b5479f1d5 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -72,14 +72,12 @@ impl App { "Failed to carry forward approval policy override: {err}" )); } - if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() - && let Err(err) = config - .permissions - .set_legacy_sandbox_policy(policy.clone(), config.cwd.as_path()) + if let Some(profile) = self.runtime_permission_profile_override.as_ref() + && let Err(err) = config.permissions.set_permission_profile(profile.clone()) { - tracing::warn!(%err, "failed to carry forward sandbox policy override"); + tracing::warn!(%err, "failed to carry forward permission profile override"); self.chat_widget.add_error_message(format!( - "Failed to carry forward sandbox policy override: {err}" + "Failed to carry forward permission profile override: {err}" )); } } @@ -106,16 +104,16 @@ impl App { true } - pub(super) fn try_set_sandbox_policy_on_config( + pub(super) fn try_set_permission_profile_on_config( &mut self, config: &mut Config, - policy: SandboxPolicy, + permission_profile: PermissionProfile, user_message_prefix: &str, log_message: &str, ) -> bool { if let Err(err) = config .permissions - .set_legacy_sandbox_policy(policy, config.cwd.as_path()) + .set_permission_profile(permission_profile) { tracing::warn!(error = %err, "{log_message}"); self.chat_widget @@ -149,7 +147,7 @@ impl App { }); let mut approval_policy_override = None; let mut approvals_reviewer_override = None; - let mut sandbox_policy_override = None; + let mut permission_profile_override = None; let mut feature_updates_to_apply = Vec::with_capacity(updates.len()); // Auto-Review owns `approvals_reviewer`, but disabling the feature // from inside a profile should not silently clear a value configured at @@ -241,11 +239,11 @@ impl App { ) { continue; } - if !self.try_set_sandbox_policy_on_config( + if !self.try_set_permission_profile_on_config( &mut feature_config, - auto_review_preset.sandbox_policy.clone(), + auto_review_preset.permission_profile.clone(), "Failed to enable Auto-review", - "failed to set auto-review sandbox policy on staged config", + "failed to set auto-review permission profile on staged config", ) { continue; } @@ -260,7 +258,7 @@ impl App { }, ]); approval_policy_override = Some(auto_review_preset.approval_policy); - sandbox_policy_override = Some(auto_review_preset.sandbox_policy.clone()); + permission_profile_override = Some(auto_review_preset.permission_profile.clone()); } next_config = feature_config; feature_updates_to_apply.push((feature, effective_enabled)); @@ -299,31 +297,26 @@ impl App { self.chat_widget .set_approval_policy(self.config.permissions.approval_policy.value()); } - if sandbox_policy_override.is_some() - && let Err(err) = self.chat_widget.set_sandbox_policy( - self.config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), - ) + if permission_profile_override.is_some() + && let Err(err) = self + .chat_widget + .set_permission_profile(self.config.permissions.permission_profile()) { tracing::error!( error = %err, - "failed to set auto-review sandbox policy on chat config" + "failed to set auto-review permission profile on chat config" ); self.chat_widget .add_error_message(format!("Failed to enable Auto-review: {err}")); } - if sandbox_policy_override.is_some() { - self.runtime_sandbox_policy_override = Some( - self.config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), - ); + if permission_profile_override.is_some() { + self.runtime_permission_profile_override = + Some(self.config.permissions.permission_profile()); } if approval_policy_override.is_some() || approvals_reviewer_override.is_some() - || sandbox_policy_override.is_some() + || permission_profile_override.is_some() { self.sync_active_thread_permission_settings_to_cached_session() .await; @@ -336,7 +329,7 @@ impl App { /*cwd*/ None, approval_policy_override, approvals_reviewer_override, - sandbox_policy_override, + permission_profile_override, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, @@ -363,7 +356,7 @@ impl App { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*sandbox_policy*/ None, + /*permission_profile*/ None, #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d249c40c8a..072efdfa99 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -703,7 +703,7 @@ impl App { AppEvent::BeginWindowsSandboxElevatedSetup { preset } => { #[cfg(target_os = "windows")] { - let policy = preset.sandbox.clone(); + let permission_profile = preset.permission_profile.clone(); let policy_cwd = self.config.cwd.clone(); let command_cwd = policy_cwd.clone(); let env_map: std::collections::HashMap = @@ -727,6 +727,19 @@ impl App { self.windows_sandbox.setup_started_at = Some(Instant::now()); let session_telemetry = self.session_telemetry.clone(); tokio::task::spawn_blocking(move || { + let policy = match permission_profile + .to_legacy_sandbox_policy(policy_cwd.as_path()) + { + Ok(policy) => policy, + Err(err) => { + tracing::error!( + %err, + "approval preset permissions cannot be projected for elevated Windows sandbox setup" + ); + tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset }); + return; + } + }; let result = crate::legacy_core::windows_sandbox::run_elevated_setup( &policy, policy_cwd.as_path(), @@ -789,7 +802,7 @@ impl App { AppEvent::BeginWindowsSandboxLegacySetup { preset } => { #[cfg(target_os = "windows")] { - let policy = preset.sandbox.clone(); + let permission_profile = preset.permission_profile.clone(); let policy_cwd = self.config.cwd.clone(); let command_cwd = policy_cwd.clone(); let env_map: std::collections::HashMap = @@ -800,6 +813,19 @@ impl App { self.chat_widget.show_windows_sandbox_setup_status(); tokio::task::spawn_blocking(move || { + let policy = match permission_profile + .to_legacy_sandbox_policy(policy_cwd.as_path()) + { + Ok(policy) => policy, + Err(err) => { + tracing::error!( + %err, + "approval preset permissions cannot be projected for legacy Windows sandbox setup" + ); + tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset }); + return; + } + }; if let Err(err) = crate::legacy_core::windows_sandbox::run_legacy_setup_preflight( &policy, @@ -935,7 +961,7 @@ impl App { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*sandbox_policy*/ None, + /*permission_profile*/ None, #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, @@ -961,7 +987,7 @@ impl App { /*cwd*/ None, Some(preset.approval), Some(self.config.approvals_reviewer), - Some(preset.sandbox.clone()), + Some(preset.permission_profile.clone()), #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, @@ -975,8 +1001,9 @@ impl App { )); self.app_event_tx .send(AppEvent::UpdateAskForApprovalPolicy(preset.approval)); - self.app_event_tx - .send(AppEvent::UpdateSandboxPolicy(preset.sandbox.clone())); + self.app_event_tx.send(AppEvent::UpdatePermissionProfile( + preset.permission_profile.clone(), + )); let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), @@ -1228,40 +1255,38 @@ impl App { self.sync_active_thread_permission_settings_to_cached_session() .await; } - AppEvent::UpdateSandboxPolicy(policy) => { + AppEvent::UpdatePermissionProfile(permission_profile) => { #[cfg(target_os = "windows")] - let policy_is_workspace_write_or_ro = matches!( - &policy, - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } - ); - let policy_for_chat = policy.clone(); + let permission_profile_is_managed_restricted = + managed_filesystem_sandbox_is_restricted(&permission_profile); + let permission_profile_for_chat = permission_profile.clone(); let mut config = self.config.clone(); - if !self.try_set_sandbox_policy_on_config( + if !self.try_set_permission_profile_on_config( &mut config, - policy, - "Failed to set sandbox policy", - "failed to set sandbox policy on app config", + permission_profile, + "Failed to set permission profile", + "failed to set permission profile on app config", ) { return Ok(AppRunControl::Continue); } self.config = config; - if let Err(err) = self.chat_widget.set_sandbox_policy(policy_for_chat) { - tracing::warn!(%err, "failed to set sandbox policy on chat config"); + if let Err(err) = self + .chat_widget + .set_permission_profile(permission_profile_for_chat) + { + tracing::warn!(%err, "failed to set permission profile on chat config"); self.chat_widget - .add_error_message(format!("Failed to set sandbox policy: {err}")); + .add_error_message(format!("Failed to set permission profile: {err}")); return Ok(AppRunControl::Continue); } - self.runtime_sandbox_policy_override = Some( - self.config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), - ); + self.runtime_permission_profile_override = + Some(self.config.permissions.permission_profile()); self.sync_active_thread_permission_settings_to_cached_session() .await; - // If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan. + // If a managed filesystem sandbox is active, run the Windows + // world-writable scan. #[cfg(target_os = "windows")] { // One-shot suppression if the user just confirmed continue. @@ -1272,7 +1297,7 @@ impl App { let should_check = WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled - && policy_is_workspace_write_or_ro + && permission_profile_is_managed_restricted && !self.chat_widget.world_writable_warning_hidden(); if should_check { let cwd = self.config.cwd.clone(); @@ -1280,15 +1305,12 @@ impl App { std::env::vars().collect(); let tx = self.app_event_tx.clone(); let logs_base_dir = self.config.codex_home.clone(); - let sandbox_policy = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); + let permission_profile = self.config.permissions.permission_profile(); Self::spawn_world_writable_scan( cwd, env_map, logs_base_dir, - sandbox_policy, + permission_profile, tx, ); } diff --git a/codex-rs/tui/src/app/platform_actions.rs b/codex-rs/tui/src/app/platform_actions.rs index b8e2888352..011d144f3b 100644 --- a/codex-rs/tui/src/app/platform_actions.rs +++ b/codex-rs/tui/src/app/platform_actions.rs @@ -18,9 +18,19 @@ impl App { cwd: AbsolutePathBuf, env_map: std::collections::HashMap, logs_base_dir: AbsolutePathBuf, - sandbox_policy: codex_protocol::protocol::SandboxPolicy, + permission_profile: PermissionProfile, tx: AppEventSender, ) { + let Ok(sandbox_policy) = permission_profile.to_legacy_sandbox_policy(cwd.as_path()) else { + tx.send(AppEvent::OpenWorldWritableWarningConfirmation { + preset: None, + sample_paths: Vec::new(), + extra_count: 0usize, + failed_scan: true, + }); + return; + }; + tokio::task::spawn_blocking(move || { let logs_base_dir_path = logs_base_dir.as_path(); let result = codex_windows_sandbox::apply_world_writable_scan_and_denies( diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index 0616d94dcd..fd88161cad 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -24,7 +24,7 @@ pub(super) async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), runtime_approval_policy_override: None, - runtime_sandbox_policy_override: None, + runtime_permission_profile_override: None, file_search, transcript_cells: Vec::new(), overlay: None, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index a543e4a35d..29f82ab85d 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -79,7 +79,6 @@ use codex_protocol::protocol::NetworkApprovalContext; use codex_protocol::protocol::NetworkApprovalProtocol; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TurnContextItem; @@ -1638,8 +1637,11 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< auto_review.approval_policy ); assert_eq!( - &app.chat_widget.config_ref().legacy_sandbox_policy(), - &auto_review.sandbox_policy + app.chat_widget + .config_ref() + .permissions + .permission_profile(), + auto_review.permission_profile ); assert_eq!( app.chat_widget.config_ref().approvals_reviewer, @@ -1647,8 +1649,8 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< ); assert_eq!(app.runtime_approval_policy_override, None); assert_eq!( - app.runtime_sandbox_policy_override, - Some(auto_review.sandbox_policy.clone()) + app.runtime_permission_profile_override, + Some(auto_review.permission_profile.clone()) ); assert_eq!( op_rx.try_recv(), @@ -1656,8 +1658,8 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - sandbox_policy: Some(auto_review.sandbox_policy.clone()), - permission_profile: None, + sandbox_policy: None, + permission_profile: Some(auto_review.permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -1714,11 +1716,12 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor .approval_policy .set(AskForApproval::OnRequest)?; app.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())?; + .permissions + .set_permission_profile(PermissionProfile::workspace_write())?; app.chat_widget .set_approval_policy(AskForApproval::OnRequest); app.chat_widget - .set_sandbox_policy(SandboxPolicy::new_workspace_write_policy())?; + .set_permission_profile(PermissionProfile::workspace_write())?; app.update_feature_flags(vec![(Feature::GuardianApproval, false)]) .await; @@ -1813,8 +1816,11 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review auto_review.approval_policy ); assert_eq!( - &app.chat_widget.config_ref().legacy_sandbox_policy(), - &auto_review.sandbox_policy + app.chat_widget + .config_ref() + .permissions + .permission_profile(), + auto_review.permission_profile ); assert_eq!( op_rx.try_recv(), @@ -1822,8 +1828,8 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - sandbox_policy: Some(auto_review.sandbox_policy.clone()), - permission_profile: None, + sandbox_policy: None, + permission_profile: Some(auto_review.permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -1940,8 +1946,8 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - sandbox_policy: Some(auto_review.sandbox_policy.clone()), - permission_profile: None, + sandbox_policy: None, + permission_profile: Some(auto_review.permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -2731,6 +2737,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re ); let rollout_path = temp_dir.path().join("agent-rollout.jsonl"); + let permission_profile = PermissionProfile::workspace_write(); let turn_context = TurnContextItem { turn_id: None, trace_id: None, @@ -2738,8 +2745,10 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re current_date: None, timezone: None, approval_policy: primary_session.approval_policy, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: None, + sandbox_policy: permission_profile + .to_legacy_sandbox_policy(test_path_buf("/tmp/agent").as_path()) + .expect("workspace profile must be legacy-compatible"), + permission_profile: Some(permission_profile), network: None, file_system_sandbox_policy: None, model: "gpt-agent".to_string(), @@ -3687,7 +3696,7 @@ async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), runtime_approval_policy_override: None, - runtime_sandbox_policy_override: None, + runtime_permission_profile_override: None, file_search, transcript_cells: Vec::new(), overlay: None, @@ -3747,7 +3756,7 @@ async fn make_test_app_with_channels() -> ( cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), runtime_approval_policy_override: None, - runtime_sandbox_policy_override: None, + runtime_permission_profile_override: None, file_search, transcript_cells: Vec::new(), overlay: None, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 345e560d41..7538209cd4 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -529,7 +529,6 @@ impl App { cwd, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, model, effort, @@ -613,7 +612,6 @@ impl App { approval_policy, approvals_reviewer .unwrap_or(self.chat_widget.config_ref().approvals_reviewer), - sandbox_policy.clone(), permission_profile.clone(), model.to_string(), effort, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 0ff1cec26b..9b6d7e9726 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -110,7 +110,6 @@ mod tests { use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::FileSystemSpecialPath; use codex_protocol::protocol::NetworkSandboxPolicy; - use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -174,23 +173,15 @@ mod tests { app.config.permissions.approval_policy = codex_config::Constrained::allow_any(AskForApproval::OnRequest); app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; - let expected_sandbox_policy = SandboxPolicy::new_workspace_write_policy(); - let expected_file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &expected_sandbox_policy, - &main_session.cwd, - ); - let expected_permission_profile = PermissionProfile::from_runtime_permissions( - &expected_file_system_policy, - NetworkSandboxPolicy::from(&expected_sandbox_policy), - ); + let expected_permission_profile = PermissionProfile::workspace_write(); app.chat_widget.handle_thread_session(main_session.clone()); app.chat_widget - .set_sandbox_policy(expected_sandbox_policy.clone()) - .expect("set widget sandbox policy"); + .set_permission_profile(expected_permission_profile.clone()) + .expect("set widget permission profile"); app.config - .set_legacy_sandbox_policy(expected_sandbox_policy.clone()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(expected_permission_profile.clone()) + .expect("set permission profile"); app.sync_active_thread_permission_settings_to_cached_session() .await; diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 1febbfa1e7..f685120132 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -17,7 +17,6 @@ use codex_protocol::protocol::ConversationTextParams; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -44,8 +43,7 @@ pub(crate) enum AppCommandView<'a> { cwd: &'a PathBuf, approval_policy: AskForApproval, approvals_reviewer: &'a Option, - sandbox_policy: &'a SandboxPolicy, - permission_profile: &'a Option, + permission_profile: &'a PermissionProfile, model: &'a str, effort: Option, summary: &'a Option, @@ -58,7 +56,7 @@ pub(crate) enum AppCommandView<'a> { cwd: &'a Option, approval_policy: &'a Option, approvals_reviewer: &'a Option, - sandbox_policy: &'a Option, + permission_profile: &'a Option, windows_sandbox_level: &'a Option, model: &'a Option, effort: &'a Option>, @@ -141,8 +139,7 @@ impl AppCommand { items: Vec, cwd: PathBuf, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, - permission_profile: Option, + permission_profile: PermissionProfile, model: String, effort: Option, summary: Option, @@ -151,6 +148,20 @@ impl AppCommand { collaboration_mode: Option, personality: Option, ) -> Self { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(cwd.as_path()) + .unwrap_or_else(|err| { + tracing::warn!( + %err, + "permission profile cannot be projected to legacy UserTurn sandbox; using read-only compatibility fallback" + ); + match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { + Ok(policy) => policy, + Err(err) => { + unreachable!("read-only permissions must be legacy-compatible: {err}") + } + } + }); Self(Op::UserTurn { items, environments: None, @@ -158,7 +169,7 @@ impl AppCommand { approval_policy, approvals_reviewer: None, sandbox_policy, - permission_profile, + permission_profile: Some(permission_profile), model, effort, summary, @@ -174,7 +185,7 @@ impl AppCommand { cwd: Option, approval_policy: Option, approvals_reviewer: Option, - sandbox_policy: Option, + permission_profile: Option, windows_sandbox_level: Option, model: Option, effort: Option>, @@ -187,8 +198,8 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer, - sandbox_policy, - permission_profile: None, + sandbox_policy: None, + permission_profile, windows_sandbox_level, model, effort, @@ -294,7 +305,7 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer, - sandbox_policy, + sandbox_policy: _, permission_profile, model, effort, @@ -309,8 +320,10 @@ impl AppCommand { cwd, approval_policy: *approval_policy, approvals_reviewer, - sandbox_policy, - permission_profile, + permission_profile: match permission_profile.as_ref() { + Some(permission_profile) => permission_profile, + None => unreachable!("AppCommand::user_turn always sets permission_profile"), + }, model, effort: *effort, summary, @@ -323,8 +336,8 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer, - sandbox_policy, - permission_profile: _, + sandbox_policy: _, + permission_profile, windows_sandbox_level, model, effort, @@ -336,7 +349,7 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer, - sandbox_policy, + permission_profile, windows_sandbox_level, model, effort, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index b30b504bb6..5c5e2bc941 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -41,9 +41,9 @@ use codex_plugin::PluginCapabilitySummary; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_realtime_webrtc::RealtimeWebrtcEvent; use codex_realtime_webrtc::RealtimeWebrtcSessionHandle; @@ -581,8 +581,8 @@ pub(crate) enum AppEvent { /// Update the current approval policy in the running app and widget. UpdateAskForApprovalPolicy(AskForApproval), - /// Update the current sandbox policy in the running app and widget. - UpdateSandboxPolicy(SandboxPolicy), + /// Update the current permission profile in the running app and widget. + UpdatePermissionProfile(PermissionProfile), /// Update the current approvals reviewer in the running app and widget. UpdateApprovalsReviewer(ApprovalsReviewer), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 0d38449db9..e9a5fb447f 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -109,7 +109,6 @@ use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget as CoreReviewTarget; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionNetworkProxyRuntime; use codex_utils_absolute_path::AbsolutePathBuf; use color_eyre::eyre::ContextCompat; @@ -532,8 +531,7 @@ impl AppServerSession { cwd: PathBuf, approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, - sandbox_policy: SandboxPolicy, - permission_profile: Option, + permission_profile: PermissionProfile, model: String, effort: Option, summary: Option, @@ -543,11 +541,32 @@ impl AppServerSession { output_schema: Option, ) -> Result { let request_id = self.next_request_id(); - let (sandbox_policy, permission_profile) = turn_start_permission_overrides( - self.thread_params_mode(), - sandbox_policy, - permission_profile, - ); + let sandbox_policy = if matches!(self.thread_params_mode(), ThreadParamsMode::Remote) { + let policy = match permission_profile.to_legacy_sandbox_policy(cwd.as_path()) { + Ok(policy) => policy, + Err(err) => { + tracing::warn!( + %err, + "permission profile cannot be projected for remote turn/start; using read-only compatibility fallback" + ); + match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { + Ok(policy) => policy, + Err(err) => { + unreachable!("read-only permissions must be legacy-compatible: {err}") + } + } + } + }; + Some(policy.into()) + } else { + None + }; + let permission_profile = if matches!(self.thread_params_mode(), ThreadParamsMode::Embedded) + { + Some(permission_profile.into()) + } else { + None + }; self.client .request_typed(ClientRequest::TurnStart { request_id, @@ -1088,35 +1107,28 @@ fn config_request_overrides_from_config( }) } -fn sandbox_mode_from_policy( - policy: SandboxPolicy, +fn sandbox_mode_from_permission_profile( + permission_profile: &PermissionProfile, + cwd: &std::path::Path, ) -> Option { - match policy { - SandboxPolicy::DangerFullAccess => { + match permission_profile { + PermissionProfile::Disabled => { Some(codex_app_server_protocol::SandboxMode::DangerFullAccess) } - SandboxPolicy::ReadOnly { .. } => Some(codex_app_server_protocol::SandboxMode::ReadOnly), - SandboxPolicy::WorkspaceWrite { .. } => { - Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) + PermissionProfile::External { .. } => None, + PermissionProfile::Managed { .. } => { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + permission_profile + .network_sandbox_policy() + .is_enabled() + .then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess) + } else if file_system_policy.can_write_path_with_cwd(cwd, cwd) { + Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) + } else { + Some(codex_app_server_protocol::SandboxMode::ReadOnly) + } } - SandboxPolicy::ExternalSandbox { .. } => None, - } -} - -fn turn_start_permission_overrides( - mode: ThreadParamsMode, - sandbox_policy: SandboxPolicy, - permission_profile: Option, -) -> ( - Option, - Option, -) { - match (mode, permission_profile) { - (ThreadParamsMode::Embedded, Some(permission_profile)) => { - (None, Some(permission_profile.into())) - } - (ThreadParamsMode::Embedded, None) => (None, None), - (ThreadParamsMode::Remote, _) => (Some(sandbox_policy.into()), None), } } @@ -1141,10 +1153,9 @@ fn thread_start_params_from_config( let sandbox = permission_profile .is_none() .then(|| { - sandbox_mode_from_policy( - config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + sandbox_mode_from_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ) }) .flatten(); @@ -1174,10 +1185,9 @@ fn thread_resume_params_from_config( let sandbox = permission_profile .is_none() .then(|| { - sandbox_mode_from_policy( - config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + sandbox_mode_from_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ) }) .flatten(); @@ -1206,10 +1216,9 @@ fn thread_fork_params_from_config( let sandbox = permission_profile .is_none() .then(|| { - sandbox_mode_from_policy( - config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + sandbox_mode_from_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ) }) .flatten(); @@ -1297,8 +1306,16 @@ async fn thread_session_state_from_thread_start_response( response.service_tier, response.approval_policy.to_core(), response.approvals_reviewer.to_core(), - response.sandbox.to_core(), - response.permission_profile.clone().map(Into::into), + response + .permission_profile + .clone() + .map(Into::into) + .unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy_for_cwd( + &response.sandbox.to_core(), + response.cwd.as_path(), + ) + }), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1321,8 +1338,16 @@ async fn thread_session_state_from_thread_resume_response( response.service_tier, response.approval_policy.to_core(), response.approvals_reviewer.to_core(), - response.sandbox.to_core(), - response.permission_profile.clone().map(Into::into), + response + .permission_profile + .clone() + .map(Into::into) + .unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy_for_cwd( + &response.sandbox.to_core(), + response.cwd.as_path(), + ) + }), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1345,8 +1370,16 @@ async fn thread_session_state_from_thread_fork_response( response.service_tier, response.approval_policy.to_core(), response.approvals_reviewer.to_core(), - response.sandbox.to_core(), - response.permission_profile.clone().map(Into::into), + response + .permission_profile + .clone() + .map(Into::into) + .unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy_for_cwd( + &response.sandbox.to_core(), + response.cwd.as_path(), + ) + }), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1388,8 +1421,7 @@ async fn thread_session_state_from_thread_response( service_tier: Option, approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, - sandbox_policy: SandboxPolicy, - permission_profile: Option, + permission_profile: PermissionProfile, cwd: AbsolutePathBuf, instruction_source_paths: Vec, reasoning_effort: Option, @@ -1404,9 +1436,6 @@ async fn thread_session_state_from_thread_response( .map_err(|err| format!("forked_from_id is invalid: {err}"))?; let (history_log_id, history_entry_count) = message_history_metadata(config).await; let history_entry_count = u64::try_from(history_entry_count).unwrap_or(u64::MAX); - let permission_profile = - permission_profile_from_response_permissions(&sandbox_policy, permission_profile, &cwd); - Ok(ThreadSessionState { thread_id, forked_from_id, @@ -1428,16 +1457,6 @@ async fn thread_session_state_from_thread_response( }) } -fn permission_profile_from_response_permissions( - sandbox_policy: &SandboxPolicy, - permission_profile: Option, - cwd: &AbsolutePathBuf, -) -> PermissionProfile { - permission_profile.unwrap_or_else(|| { - PermissionProfile::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.as_path()) - }) -} - pub(crate) fn app_server_rate_limit_snapshots_to_core( response: GetAccountRateLimitsResponse, ) -> Vec { @@ -1548,10 +1567,9 @@ mod tests { let temp_dir = tempfile::tempdir().expect("tempdir"); let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); - let expected_sandbox = sandbox_mode_from_policy( - config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + let expected_sandbox = sandbox_mode_from_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ); let start = thread_start_params_from_config( @@ -1593,10 +1611,9 @@ mod tests { let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); let remote_cwd = PathBuf::from("repo/on/server"); - let expected_sandbox = sandbox_mode_from_policy( - config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + let expected_sandbox = sandbox_mode_from_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ); let start = thread_start_params_from_config( @@ -1632,55 +1649,6 @@ mod tests { assert_eq!(fork.permission_profile, None); } - #[test] - fn turn_start_permission_overrides_send_profiles_only_for_embedded_runtime_overrides() { - let workspace_write = SandboxPolicy::new_workspace_write_policy(); - let workspace_write_profile = - PermissionProfile::from_legacy_sandbox_policy(&workspace_write); - - let (sandbox, profile) = turn_start_permission_overrides( - ThreadParamsMode::Embedded, - workspace_write.clone(), - Some(workspace_write_profile.clone()), - ); - assert_eq!(sandbox, None); - assert_eq!(profile, Some(workspace_write_profile.into())); - - let (sandbox, profile) = turn_start_permission_overrides( - ThreadParamsMode::Embedded, - workspace_write.clone(), - /*permission_profile*/ None, - ); - assert_eq!(sandbox, None); - assert_eq!(profile, None); - - let (sandbox, profile) = turn_start_permission_overrides( - ThreadParamsMode::Remote, - workspace_write.clone(), - Some(PermissionProfile::from_legacy_sandbox_policy( - &workspace_write, - )), - ); - assert_eq!(sandbox, Some(workspace_write.into())); - assert_eq!(profile, None); - - let external_sandbox = SandboxPolicy::ExternalSandbox { - network_access: codex_protocol::protocol::NetworkAccess::Restricted, - }; - let (sandbox, profile) = turn_start_permission_overrides( - ThreadParamsMode::Embedded, - external_sandbox.clone(), - Some(PermissionProfile::from_legacy_sandbox_policy( - &external_sandbox, - )), - ); - assert_eq!(sandbox, None); - assert_eq!( - profile, - Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox).into()) - ); - } - #[tokio::test] async fn thread_fork_params_forward_instruction_overrides() { let temp_dir = tempfile::tempdir().expect("tempdir"); @@ -1709,6 +1677,7 @@ mod tests { let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); let forked_from_id = ThreadId::new(); + let read_only_profile = PermissionProfile::read_only(); let response = ThreadResumeResponse { thread: codex_app_server_protocol::Thread { id: thread_id.to_string(), @@ -1758,13 +1727,11 @@ mod tests { instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()], approval_policy: codex_protocol::protocol::AskForApproval::Never.into(), approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User, - sandbox: codex_protocol::protocol::SandboxPolicy::new_read_only_policy().into(), - permission_profile: Some( - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), - ) + sandbox: read_only_profile + .to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path()) + .expect("read-only profile must be legacy-compatible") .into(), - ), + permission_profile: Some(read_only_profile.into()), reasoning_effort: None, }; @@ -1811,8 +1778,7 @@ mod tests { /*service_tier*/ None, AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, - SandboxPolicy::new_read_only_policy(), - Some(PermissionProfile::read_only()), + PermissionProfile::read_only(), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, @@ -1842,8 +1808,7 @@ mod tests { /*service_tier*/ None, AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, - SandboxPolicy::new_read_only_policy(), - Some(PermissionProfile::read_only()), + PermissionProfile::read_only(), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fc2f200850..e8486a08e9 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -422,13 +422,13 @@ use crate::streaming::controller::StreamController; use chrono::Local; use codex_file_search::FileMatch; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::StepStatus; use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_approval_presets::ApprovalPreset; use codex_utils_approval_presets::builtin_approval_presets; use strum::IntoEnumIterator; @@ -6480,14 +6480,11 @@ impl ChatWidget { None if self.config.notices.fast_default_opt_out == Some(true) => Some(None), None => None, }; - let permission_profile = Some(self.config.permissions.permission_profile()); + let permission_profile = self.config.permissions.permission_profile(); let op = AppCommand::user_turn( items, self.config.cwd.to_path_buf(), self.config.permissions.approval_policy.value(), - self.config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), permission_profile, effective_mode.model().to_string(), effective_mode.reasoning_effort(), @@ -8567,7 +8564,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*sandbox_policy*/ None, + /*permission_profile*/ None, /*windows_sandbox_level*/ None, Some(switch_model_for_events.clone()), Some(Some(default_effort)), @@ -8791,7 +8788,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*sandbox_policy*/ None, + /*permission_profile*/ None, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, @@ -9540,14 +9537,11 @@ impl ChatWidget { self.open_permissions_popup(); } - /// Open a popup to choose the permissions mode (approval policy + sandbox policy). + /// Open a popup to choose the permissions mode. pub(crate) fn open_permissions_popup(&mut self) { let include_read_only = cfg!(target_os = "windows"); let current_approval = self.config.permissions.approval_policy.value(); - let current_sandbox = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); + let current_permission_profile = self.config.permissions.permission_profile(); let guardian_approval_enabled = self.config.features.enabled(Feature::GuardianApproval); let current_review_policy = self.config.approvals_reviewer; let mut items: Vec = Vec::new(); @@ -9653,7 +9647,7 @@ impl ChatWidget { } else { Self::approval_preset_actions( preset.approval, - preset.sandbox.clone(), + preset.permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -9663,7 +9657,7 @@ impl ChatWidget { { Self::approval_preset_actions( preset.approval, - preset.sandbox.clone(), + preset.permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -9671,7 +9665,7 @@ impl ChatWidget { } else { Self::approval_preset_actions( preset.approval, - preset.sandbox.clone(), + preset.permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -9683,7 +9677,8 @@ impl ChatWidget { is_current: current_review_policy == ApprovalsReviewer::User && Self::preset_matches_current( current_approval, - ¤t_sandbox, + ¤t_permission_profile, + self.config.cwd.as_path(), &preset, ), actions: default_actions, @@ -9701,13 +9696,14 @@ impl ChatWidget { ), is_current: current_review_policy == ApprovalsReviewer::AutoReview && Self::preset_matches_current( - current_approval, - ¤t_sandbox, - &preset, - ), + current_approval, + ¤t_permission_profile, + self.config.cwd.as_path(), + &preset, + ), actions: Self::approval_preset_actions( preset.approval, - preset.sandbox.clone(), + preset.permission_profile.clone(), "Auto-review".to_string(), ApprovalsReviewer::AutoReview, ), @@ -9723,7 +9719,8 @@ impl ChatWidget { description: base_description, is_current: Self::preset_matches_current( current_approval, - ¤t_sandbox, + ¤t_permission_profile, + self.config.cwd.as_path(), &preset, ), actions: default_actions, @@ -9848,18 +9845,18 @@ impl ChatWidget { fn approval_preset_actions( approval: AskForApproval, - sandbox: SandboxPolicy, + permission_profile: PermissionProfile, label: String, approvals_reviewer: ApprovalsReviewer, ) -> Vec { vec![Box::new(move |tx| { - let sandbox_clone = sandbox.clone(); + let permission_profile_clone = permission_profile.clone(); tx.send(AppEvent::CodexOp( AppCommand::override_turn_context( /*cwd*/ None, Some(approval), Some(approvals_reviewer), - Some(sandbox_clone.clone()), + Some(permission_profile_clone.clone()), /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, @@ -9871,7 +9868,7 @@ impl ChatWidget { .into_core(), )); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); - tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone)); + tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone)); tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); tx.send(AppEvent::InsertHistoryCell(Box::new( history_cell::new_info_event( @@ -9884,36 +9881,39 @@ impl ChatWidget { fn preset_matches_current( current_approval: AskForApproval, - current_sandbox: &SandboxPolicy, + current_permission_profile: &PermissionProfile, + cwd: &std::path::Path, preset: &ApprovalPreset, ) -> bool { if current_approval != preset.approval { return false; } - match (current_sandbox, &preset.sandbox) { - (SandboxPolicy::DangerFullAccess, SandboxPolicy::DangerFullAccess) => true, - ( - SandboxPolicy::ReadOnly { - network_access: current_network_access, - .. - }, - SandboxPolicy::ReadOnly { - network_access: preset_network_access, - .. - }, - ) => current_network_access == preset_network_access, - ( - SandboxPolicy::WorkspaceWrite { - network_access: current_network_access, - .. - }, - SandboxPolicy::WorkspaceWrite { - network_access: preset_network_access, - .. - }, - ) => current_network_access == preset_network_access, - _ => false, + match preset.id { + "full-access" => matches!(current_permission_profile, PermissionProfile::Disabled), + "read-only" => { + let file_system_policy = current_permission_profile.file_system_sandbox_policy(); + matches!( + current_permission_profile, + PermissionProfile::Managed { .. } + ) && !file_system_policy.has_full_disk_write_access() + && file_system_policy + .get_writable_roots_with_cwd(cwd) + .is_empty() + && current_permission_profile.network_sandbox_policy() + == preset.permission_profile.network_sandbox_policy() + } + "auto" => { + let file_system_policy = current_permission_profile.file_system_sandbox_policy(); + matches!( + current_permission_profile, + PermissionProfile::Managed { .. } + ) && file_system_policy.can_write_path_with_cwd(cwd, cwd) + && !file_system_policy.has_full_disk_write_access() + && current_permission_profile.network_sandbox_policy() + == preset.permission_profile.network_sandbox_policy() + } + _ => current_permission_profile == &preset.permission_profile, } } @@ -9929,14 +9929,19 @@ impl ChatWidget { } let cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); + let Ok(policy) = self + .config + .permissions + .permission_profile() + .to_legacy_sandbox_policy(self.config.cwd.as_path()) + else { + return Some((Vec::new(), 0, true)); + }; match codex_windows_sandbox::apply_world_writable_scan_and_denies( self.config.codex_home.as_path(), cwd.as_path(), &env_map, - &self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), + &policy, Some(self.config.codex_home.as_path()), ) { Ok(_) => None, @@ -9957,7 +9962,7 @@ impl ChatWidget { ) { let selected_name = preset.label.to_string(); let approval = preset.approval; - let sandbox = preset.sandbox; + let permission_profile = preset.permission_profile; let mut header_children: Vec> = Vec::new(); let title_line = Line::from("Enable full access?").bold(); let info_line = Line::from(vec![ @@ -9974,7 +9979,7 @@ impl ChatWidget { let mut accept_actions = Self::approval_preset_actions( approval, - sandbox.clone(), + permission_profile.clone(), selected_name.clone(), ApprovalsReviewer::User, ); @@ -9984,7 +9989,7 @@ impl ChatWidget { let mut accept_and_remember_actions = Self::approval_preset_actions( approval, - sandbox, + permission_profile, selected_name, ApprovalsReviewer::User, ); @@ -10041,27 +10046,27 @@ impl ChatWidget { extra_count: usize, failed_scan: bool, ) { - let (approval, sandbox) = match &preset { - Some(p) => (Some(p.approval), Some(p.sandbox.clone())), + let (approval, permission_profile) = match &preset { + Some(p) => (Some(p.approval), Some(p.permission_profile.clone())), None => (None, None), }; let mut header_children: Vec> = Vec::new(); - let describe_policy = |policy: &SandboxPolicy| match policy { - SandboxPolicy::WorkspaceWrite { .. } => "Agent mode", - SandboxPolicy::ReadOnly { .. } => "Read-Only mode", - _ => "Agent mode", + let describe_profile = |profile: &PermissionProfile| { + if matches!(profile, PermissionProfile::Disabled) { + "Full Access mode" + } else if profile + .file_system_sandbox_policy() + .can_write_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path()) + { + "Agent mode" + } else { + "Read-Only mode" + } }; let mode_label = preset .as_ref() - .map(|p| describe_policy(&p.sandbox)) - .unwrap_or_else(|| { - describe_policy( - &self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()), - ) - }); + .map(|p| describe_profile(&p.permission_profile)) + .unwrap_or_else(|| describe_profile(&self.config.permissions.permission_profile())); let info_line = if failed_scan { Line::from(vec![ "We couldn't complete the world-writable scan, so protections cannot be verified. " @@ -10093,8 +10098,9 @@ impl ChatWidget { } let header = ColumnRenderable::with(header_children); - // Build actions ensuring acknowledgement happens before applying the new sandbox policy, - // so downstream policy-change hooks don't re-trigger the warning. + // Build actions ensuring acknowledgement happens before applying the + // new permission profile, so downstream policy-change hooks don't + // re-trigger the warning. let mut accept_actions: Vec = Vec::new(); // Suppress the immediate re-scan only when a preset will be applied (i.e., via /approvals or // /permissions), to avoid duplicate warnings from the ensuing policy change. @@ -10103,10 +10109,10 @@ impl ChatWidget { tx.send(AppEvent::SkipNextWorldWritableScan); })); } - if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) { + if let (Some(approval), Some(permission_profile)) = (approval, permission_profile.clone()) { accept_actions.extend(Self::approval_preset_actions( approval, - sandbox, + permission_profile, mode_label.to_string(), ApprovalsReviewer::User, )); @@ -10117,10 +10123,10 @@ impl ChatWidget { tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true)); tx.send(AppEvent::PersistWorldWritableWarningAcknowledged); })); - if let (Some(approval), Some(sandbox)) = (approval, sandbox) { + if let (Some(approval), Some(permission_profile)) = (approval, permission_profile) { accept_and_remember_actions.extend(Self::approval_preset_actions( approval, - sandbox, + permission_profile, mode_label.to_string(), ApprovalsReviewer::User, )); @@ -10439,12 +10445,13 @@ impl ChatWidget { } } - /// Set the sandbox policy in the widget's config copy. + /// Set the permission profile in the widget's config copy. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { - self.config - .permissions - .set_legacy_sandbox_policy(policy, self.config.cwd.as_path()) + pub(crate) fn set_permission_profile( + &mut self, + profile: PermissionProfile, + ) -> ConstraintResult<()> { + self.config.permissions.set_permission_profile(profile) } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] @@ -10701,7 +10708,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*sandbox_policy*/ None, + /*permission_profile*/ None, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 443cac0532..86346885b6 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -5,7 +5,6 @@ use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSandboxKind; use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::FileSystemSpecialPath; -use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; @@ -249,8 +248,9 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); chat.config.cwd = test_path_buf("/home/user/main").abs(); let expected_cwd = test_path_buf("/home/user/sub-agent").abs(); @@ -314,23 +314,13 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { ); assert_eq!(&chat.config_ref().cwd, &expected_cwd); - let updated_sandbox = SandboxPolicy::new_workspace_write_policy(); - chat.set_sandbox_policy(updated_sandbox.clone()) - .expect("set sandbox policy"); - let updated_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &updated_sandbox, - &expected_cwd, - ); + let updated_profile = PermissionProfile::workspace_write(); + chat.set_permission_profile(updated_profile.clone()) + .expect("set permission profile"); assert_eq!( chat.config_ref().permissions.permission_profile(), - codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( - codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy( - &updated_sandbox - ), - &updated_file_system_policy, - NetworkSandboxPolicy::from(&updated_sandbox), - ), - "local sandbox changes should replace SessionConfigured profile-derived runtime permissions using the widget cwd" + updated_profile, + "local permission changes should replace SessionConfigured profile-derived runtime permissions" ); } @@ -338,9 +328,12 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { async fn session_configured_external_sandbox_keeps_external_runtime_policy() { let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - let expected_sandbox = SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, + let expected_permission_profile = PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, }; + let expected_sandbox = expected_permission_profile + .to_legacy_sandbox_policy(test_path_buf("/home/user/external").as_path()) + .expect("external profile should project to legacy sandbox policy"); let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: ThreadId::new(), forked_from_id: None, @@ -350,9 +343,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::External { - network: NetworkSandboxPolicy::Restricted, - }, + permission_profile: expected_permission_profile, cwd: test_path_buf("/home/user/external").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 4666e68049..829fa77f5d 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -1,4 +1,10 @@ use super::*; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; #[tokio::test] @@ -51,24 +57,73 @@ async fn preset_matching_accepts_workspace_write_with_extra_roots() { .into_iter() .find(|p| p.id == "auto") .expect("auto preset exists"); - let extra_root = test_path_buf("/tmp/extra").abs(); - let current_sandbox = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![extra_root], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; + let current_profile = PermissionProfile::workspace_write_with( + &[test_path_buf("/tmp/extra").abs()], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); + let cwd = test_path_buf("/tmp/project").abs(); assert!( - ChatWidget::preset_matches_current(AskForApproval::OnRequest, ¤t_sandbox, &preset), + ChatWidget::preset_matches_current( + AskForApproval::OnRequest, + ¤t_profile, + cwd.as_path(), + &preset + ), "WorkspaceWrite with extra roots should still match the Default preset" ); assert!( - !ChatWidget::preset_matches_current(AskForApproval::Never, ¤t_sandbox, &preset), + !ChatWidget::preset_matches_current( + AskForApproval::Never, + ¤t_profile, + cwd.as_path(), + &preset + ), "approval mismatch should prevent matching the preset" ); } +#[tokio::test] +async fn preset_matching_does_not_treat_non_cwd_writable_profile_as_read_only() { + let preset = builtin_approval_presets() + .into_iter() + .find(|p| p.id == "read-only") + .expect("read-only preset exists"); + let current_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: test_path_buf("/tmp/writable").abs(), + }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + let cwd = test_path_buf("/tmp/project").abs(); + + assert!( + !ChatWidget::preset_matches_current( + AskForApproval::OnRequest, + ¤t_profile, + cwd.as_path(), + &preset + ), + "profiles with any writable root should not be classified as Read Only" + ); +} + #[tokio::test] async fn full_access_confirmation_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -348,8 +403,9 @@ async fn permissions_selection_history_snapshot_full_access_to_default() { .set(AskForApproval::Never) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -389,8 +445,9 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() { .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); @@ -446,8 +503,9 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_a .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -521,7 +579,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w let cwd = test_project_path().abs(); let permission_profile = PermissionProfile::workspace_write_with( &[extra_root], - codex_protocol::protocol::NetworkSandboxPolicy::Restricted, + NetworkSandboxPolicy::Restricted, /*exclude_tmpdir_env_var*/ false, /*exclude_slash_tmp*/ false, ); @@ -573,8 +631,9 @@ async fn permissions_selection_can_disable_auto_review() { .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Up)); @@ -612,8 +671,9 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); chat.set_approvals_reviewer(ApprovalsReviewer::User); chat.open_permissions_popup(); @@ -648,8 +708,8 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context cwd: None, approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(ApprovalsReviewer::AutoReview), - sandbox_policy: Some(SandboxPolicy::new_workspace_write_policy()), - permission_profile: None, + sandbox_policy: None, + permission_profile: Some(PermissionProfile::workspace_write()), windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index d098767398..977fcc4b6d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -882,9 +882,8 @@ pub async fn run_main( if let Some(warning) = add_dir_warning_message( &cli.add_dir, - &config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + &config.permissions.permission_profile(), + config.cwd.as_path(), ) { #[allow(clippy::print_stderr)] { @@ -2211,6 +2210,10 @@ mod tests { .model .clone() .unwrap_or_else(|| "gpt-5.1".to_string()); + let permission_profile = config.permissions.permission_profile(); + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(config.cwd.as_path()) + .expect("configured permissions must have a legacy compatibility projection"); TurnContextItem { turn_id: None, trace_id: None, @@ -2218,10 +2221,8 @@ mod tests { current_date: None, timezone: None, approval_policy: config.permissions.approval_policy.value(), - sandbox_policy: config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), - permission_profile: None, + sandbox_policy, + permission_profile: Some(permission_profile), network: None, file_system_sandbox_policy: None, model, diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 06594587b6..b9366df208 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -9,16 +9,16 @@ use chrono::Local; use codex_model_provider_info::WireApi; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; -use codex_utils_sandbox_summary::summarize_sandbox_policy; +use codex_utils_sandbox_summary::summarize_permission_profile; use ratatui::prelude::*; use ratatui::style::Stylize; use std::collections::BTreeSet; +use std::path::Path; use std::path::PathBuf; use url::Url; @@ -254,10 +254,9 @@ impl StatusHistoryCell { ), ( "sandbox", - summarize_sandbox_policy( - &config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + summarize_permission_profile( + &config.permissions.permission_profile(), + config.cwd.as_path(), ), ), ]; @@ -281,31 +280,14 @@ impl StatusHistoryCell { .find(|(k, _)| *k == "approval") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "".to_string()); - let sandbox_policy = config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()); - let sandbox = match &sandbox_policy { - SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), - SandboxPolicy::ReadOnly { .. } => "read-only".to_string(), - SandboxPolicy::WorkspaceWrite { - network_access: true, - .. - } => "workspace-write with network access".to_string(), - SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(), - SandboxPolicy::ExternalSandbox { network_access } => { - if matches!(network_access, NetworkAccess::Enabled) { - "external-sandbox (network access enabled)".to_string() - } else { - "external-sandbox".to_string() - } - } - }; + let permission_profile = config.permissions.permission_profile(); + let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path()); let permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest - && sandbox_policy == SandboxPolicy::new_workspace_write_policy() + && permission_profile == PermissionProfile::workspace_write() { "Default".to_string() } else if config.permissions.approval_policy.value() == AskForApproval::Never - && sandbox_policy == SandboxPolicy::DangerFullAccess + && permission_profile == PermissionProfile::Disabled { "Full Access".to_string() } else { @@ -553,6 +535,20 @@ impl StatusHistoryCell { } } +fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path) -> String { + let summary = summarize_permission_profile(permission_profile, cwd); + if let Some(details) = summary.strip_prefix("workspace-write") { + if details.contains("(network access enabled)") { + return "workspace-write with network access".to_string(); + } + return "workspace-write".to_string(); + } + if summary == "custom permissions (network access enabled)" { + return "custom permissions with network access".to_string(); + } + summary +} + impl HistoryCell for StatusHistoryCell { fn display_lines(&self, width: u16) -> Vec> { let mut lines: Vec> = Vec::new(); diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap index 4c2018abf1..ae85660b9e 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap @@ -4,20 +4,20 @@ expression: sanitized --- /status -╭─────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 1.05K total (700 input + 350 output) │ -│ Context window: 100% left (1.45K used / 272K) │ -│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │ -│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │ -│ Warning: limits may be stale - start new turn to refresh. │ -╰─────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 1.05K total (700 input + 350 output) │ +│ Context window: 100% left (1.45K used / 272K) │ +│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │ +│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │ +│ Warning: limits may be stale - start new turn to refresh. │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap index 6cc7a8946c..f01a626111 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap @@ -4,20 +4,20 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 2K total (1.4K input + 600 output) │ -│ Context window: 100% left (2.2K used / 272K) │ -│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │ -│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │ -│ Credits: 38 credits │ -╰───────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 2K total (1.4K input + 600 output) │ +│ Context window: 100% left (2.2K used / 272K) │ +│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │ +│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │ +│ Credits: 38 credits │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap index fa0df32ed4..b473b4cd0c 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap @@ -4,20 +4,20 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │ -│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │ -│ │ -│ Token usage: 1.2K total (800 input + 400 output) │ -│ Context window: 100% left (1.2K used / 272K) │ -│ Limits: data not available yet │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │ +│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │ +│ │ +│ Token usage: 1.2K total (800 input + 400 output) │ +│ Context window: 100% left (1.2K used / 272K) │ +│ Limits: data not available yet │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap index 027d935c70..e8be998997 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap @@ -4,18 +4,18 @@ expression: sanitized --- /status -╭────────────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 1.2K total (800 input + 400 output) │ -│ Context window: 100% left (1.2K used / 272K) │ -│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May) │ -╰────────────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 1.2K total (800 input + 400 output) │ +│ Context window: 100% left (1.2K used / 272K) │ +│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May) │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap index 6db1821d4c..948ab6ef7b 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap @@ -4,18 +4,18 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Context window: 100% left (750 used / 272K) │ -│ Limits: data not available yet │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 used / 272K) │ +│ Limits: data not available yet │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_refreshing_limits_notice.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_refreshing_limits_notice.snap index 2b31afce0c..ad81d5da7b 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_refreshing_limits_notice.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_refreshing_limits_notice.snap @@ -4,19 +4,19 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Context window: 100% left (750 used / 272K) │ -│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24) │ -│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54) │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 used / 272K) │ +│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24) │ +│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54) │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap index e6043b65f3..e07b400e72 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap @@ -4,20 +4,20 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 1.9K total (1K input + 900 output) │ -│ Context window: 100% left (2.25K used / 272K) │ -│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │ -│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │ -│ Warning: limits may be stale - start new turn to refresh. │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 1.9K total (1K input + 900 output) │ +│ Context window: 100% left (2.25K used / 272K) │ +│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │ +│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │ +│ Warning: limits may be stale - start new turn to refresh. │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_unavailable_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_unavailable_limits_message.snap index 62c6cc3375..99c2d32268 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_unavailable_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_unavailable_limits_message.snap @@ -4,18 +4,18 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Context window: 100% left (750 used / 272K) │ -│ Limits: not available for this account │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 used / 272K) │ +│ Limits: not available for this account │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_treats_refreshing_empty_limits_as_unavailable.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_treats_refreshing_empty_limits_as_unavailable.snap index 62c6cc3375..99c2d32268 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_treats_refreshing_empty_limits_as_unavailable.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_treats_refreshing_empty_limits_as_unavailable.snap @@ -4,18 +4,18 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Context window: 100% left (750 used / 272K) │ -│ Limits: not available for this account │ -╰───────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 used / 272K) │ +│ Limits: not available for this account │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap index 91057d482b..0a342b5f13 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap @@ -12,7 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning high, summaries de │ │ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ +│ Permissions: Custom (workspace-write with network access, on │ │ Agents.md: │ │ │ │ Token usage: 1.9K total (1K input + 900 output) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_uses_default_reasoning_when_config_empty.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_uses_default_reasoning_when_config_empty.snap index de0ea5056d..e0ceb5b680 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_uses_default_reasoning_when_config_empty.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_uses_default_reasoning_when_config_empty.snap @@ -4,18 +4,18 @@ expression: sanitized --- /status -╭─────────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ -│ information on rate limits and credits │ -│ │ -│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │ -│ Directory: [[workspace]] │ -│ Permissions: Custom (read-only, on-request) │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Context window: 100% left (750 used / 272K) │ -│ Limits: data not available yet │ -╰─────────────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ +│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │ +│ Directory: [[workspace]] │ +│ Permissions: Custom (workspace-write with network access, on-request) │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 used / 272K) │ +│ Limits: data not available yet │ +╰─────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 03a988c793..b6b38cad10 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -13,12 +13,14 @@ use chrono::TimeZone; use chrono::Utc; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CreditsSnapshot; +use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; use insta::assert_snapshot; @@ -27,11 +29,21 @@ use ratatui::prelude::*; use tempfile::TempDir; async fn test_config(temp_home: &TempDir) -> Config { - ConfigBuilder::default() + let mut config = ConfigBuilder::default() .codex_home(temp_home.path().to_path_buf()) .build() .await - .expect("load config") + .expect("load config"); + config + .permissions + .set_permission_profile(PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + )) + .expect("set permission profile"); + config } fn test_status_account_display() -> Option { @@ -90,6 +102,41 @@ fn reset_at_from(captured_at: &chrono::DateTime, seconds: i64) -> .timestamp() } +fn permissions_text_for(config: &Config) -> Option { + let usage = TokenUsage::default(); + let captured_at = chrono::Local + .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) + .single() + .expect("timestamp"); + let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref()); + let composite = new_status_output( + config, + test_status_account_display().as_ref(), + /*token_info*/ None, + &usage, + &None, + /*thread_name*/ None, + /*forked_from*/ None, + /*rate_limits*/ None, + None, + captured_at, + &model_slug, + /*collaboration_mode*/ None, + /*reasoning_effort_override*/ None, + ); + render_lines(&composite.display_lines(/*width*/ 80)) + .iter() + .find(|line| line.contains("Permissions:")) + .and_then(|line| { + line.split("Permissions:") + .nth(1) + .map(str::trim) + .map(|text| text.trim_end_matches('│')) + .map(str::trim) + .map(ToString::to_string) + }) +} + #[tokio::test] async fn status_snapshot_includes_reasoning_details() { let temp_home = TempDir::new().expect("temp home"); @@ -99,13 +146,9 @@ async fn status_snapshot_includes_reasoning_details() { config.model_reasoning_summary = Some(ReasoningSummary::Detailed); config.cwd = test_path_buf("/workspace/tests").abs(); config - .set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set permission profile"); let account_display = test_status_account_display(); let usage = TokenUsage { @@ -181,55 +224,67 @@ async fn status_permissions_non_default_workspace_write_is_custom() { .expect("set approval policy"); config.cwd = test_path_buf("/workspace/tests").abs(); config - .set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) - .expect("set sandbox policy"); - - let account_display = test_status_account_display(); - let usage = TokenUsage::default(); - let captured_at = chrono::Local - .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) - .single() - .expect("timestamp"); - let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref()); - - let composite = new_status_output( - &config, - account_display.as_ref(), - /*token_info*/ None, - &usage, - &None, - /*thread_name*/ None, - /*forked_from*/ None, - /*rate_limits*/ None, - None, - captured_at, - &model_slug, - /*collaboration_mode*/ None, - /*reasoning_effort_override*/ None, - ); - let rendered_lines = render_lines(&composite.display_lines(/*width*/ 80)); - let permissions_line = rendered_lines - .iter() - .find(|line| line.contains("Permissions:")) - .expect("permissions line"); - let permissions_text = permissions_line - .split("Permissions:") - .nth(1) - .map(str::trim) - .map(|text| text.trim_end_matches('│')) - .map(str::trim); + .permissions + .set_permission_profile(PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Enabled, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + )) + .expect("set permission profile"); assert_eq!( - permissions_text, + permissions_text_for(&config).as_deref(), Some("Custom (workspace-write with network access, on-request)") ); } +#[tokio::test] +async fn status_permissions_full_disk_managed_with_network_is_danger_full_access() { + let temp_home = TempDir::new().expect("temp home"); + let mut config = test_config(&temp_home).await; + config + .permissions + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); + config + .permissions + .set_permission_profile(PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }) + .expect("set permission profile"); + + assert_eq!( + permissions_text_for(&config).as_deref(), + Some("Custom (danger-full-access, on-request)") + ); +} + +#[tokio::test] +async fn status_permissions_full_disk_managed_without_network_is_external_sandbox() { + let temp_home = TempDir::new().expect("temp home"); + let mut config = test_config(&temp_home).await; + config + .permissions + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); + config + .permissions + .set_permission_profile(PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Restricted, + }) + .expect("set permission profile"); + + assert_eq!( + permissions_text_for(&config).as_deref(), + Some("Custom (external-sandbox, on-request)") + ); +} + #[tokio::test] async fn status_snapshot_includes_forked_from() { let temp_home = TempDir::new().expect("temp home"); diff --git a/codex-rs/utils/approval-presets/src/lib.rs b/codex-rs/utils/approval-presets/src/lib.rs index fbfa120e61..b254950500 100644 --- a/codex-rs/utils/approval-presets/src/lib.rs +++ b/codex-rs/utils/approval-presets/src/lib.rs @@ -1,7 +1,7 @@ +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; -/// A simple preset pairing an approval policy with a sandbox policy. +/// A simple preset pairing an approval policy with a permission profile. #[derive(Debug, Clone)] pub struct ApprovalPreset { /// Stable identifier for the preset. @@ -12,11 +12,11 @@ pub struct ApprovalPreset { pub description: &'static str, /// Approval policy to apply. pub approval: AskForApproval, - /// Sandbox policy to apply. - pub sandbox: SandboxPolicy, + /// Permission profile to apply. + pub permission_profile: PermissionProfile, } -/// Built-in list of approval presets that pair approval and sandbox policy. +/// Built-in list of approval presets that pair approval and permissions. /// /// Keep this UI-agnostic so it can be reused by both TUI and MCP server. pub fn builtin_approval_presets() -> Vec { @@ -26,21 +26,21 @@ pub fn builtin_approval_presets() -> Vec { label: "Read Only", description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.", approval: AskForApproval::OnRequest, - sandbox: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), }, ApprovalPreset { id: "auto", label: "Default", description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files. (Identical to Agent mode)", approval: AskForApproval::OnRequest, - sandbox: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::workspace_write(), }, ApprovalPreset { id: "full-access", label: "Full Access", description: "Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using.", approval: AskForApproval::Never, - sandbox: SandboxPolicy::DangerFullAccess, + permission_profile: PermissionProfile::Disabled, }, ] } diff --git a/codex-rs/utils/sandbox-summary/src/lib.rs b/codex-rs/utils/sandbox-summary/src/lib.rs index 0e5e09af62..f3da7ab91d 100644 --- a/codex-rs/utils/sandbox-summary/src/lib.rs +++ b/codex-rs/utils/sandbox-summary/src/lib.rs @@ -2,4 +2,5 @@ mod config_summary; mod sandbox_summary; pub use config_summary::create_config_summary_entries; +pub use sandbox_summary::summarize_permission_profile; pub use sandbox_summary::summarize_sandbox_policy; diff --git a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs index f9a4f5daf1..0719773aad 100644 --- a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs @@ -1,5 +1,7 @@ +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::SandboxPolicy; +use std::path::Path; pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { match sandbox_policy { @@ -49,6 +51,19 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { } } +pub fn summarize_permission_profile(permission_profile: &PermissionProfile, cwd: &Path) -> String { + match permission_profile.to_legacy_sandbox_policy(cwd) { + Ok(policy) => summarize_sandbox_policy(&policy), + Err(_) => { + if permission_profile.network_sandbox_policy().is_enabled() { + "custom permissions (network access enabled)".to_string() + } else { + "custom permissions".to_string() + } + } + } +} + #[cfg(test)] mod tests { use super::*; From 49552cf307c3dfa5f3c45dff8d38c72e0fc8e76c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 02/20] core tests: submit turns with permission profiles --- codex-rs/core/tests/common/test_codex.rs | 74 +++++++++++-- codex-rs/core/tests/suite/openai_file_mcp.rs | 6 +- codex-rs/core/tests/suite/search_tool.rs | 34 +++--- .../core/tests/suite/shell_serialization.rs | 66 ++++++------ codex-rs/core/tests/suite/tool_suggest.rs | 6 +- codex-rs/core/tests/suite/truncation.rs | 44 +++++--- codex-rs/core/tests/suite/web_search.rs | 24 ++--- codex-rs/tui/src/app_command.rs | 17 ++- codex-rs/tui/src/app_server_session.rs | 65 ++++++++--- codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/permission_compat.rs | 102 ++++++++++++++++++ 11 files changed, 322 insertions(+), 117 deletions(-) create mode 100644 codex-rs/tui/src/permission_compat.rs diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index d77d55956a..fd92d0fde1 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -28,6 +28,7 @@ use codex_model_provider_info::built_in_model_providers; use codex_models_manager::bundled_models_response; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -591,10 +592,19 @@ impl TestCodex { } pub async fn submit_turn(&self, prompt: &str) -> Result<()> { - self.submit_turn_with_policies( + self.submit_turn_with_permission_profile(prompt, PermissionProfile::Disabled) + .await + } + + pub async fn submit_turn_with_permission_profile( + &self, + prompt: &str, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.submit_turn_with_approval_and_permission_profile( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + permission_profile, ) .await } @@ -613,10 +623,10 @@ impl TestCodex { prompt: &str, service_tier: Option, ) -> Result<()> { - self.submit_turn_with_context( + self.submit_turn_with_permission_profile_context( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, Some(service_tier), /*environments*/ None, ) @@ -633,6 +643,23 @@ impl TestCodex { prompt, approval_policy, sandbox_policy, + /*permission_profile*/ None, + /*service_tier*/ None, + /*environments*/ None, + ) + .await + } + + pub async fn submit_turn_with_approval_and_permission_profile( + &self, + prompt: &str, + approval_policy: AskForApproval, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.submit_turn_with_permission_profile_context( + prompt, + approval_policy, + permission_profile, /*service_tier*/ None, /*environments*/ None, ) @@ -644,21 +671,44 @@ impl TestCodex { prompt: &str, environments: Option>, ) -> Result<()> { - self.submit_turn_with_context( + self.submit_turn_with_permission_profile_context( prompt, AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, /*service_tier*/ None, environments, ) .await } + async fn submit_turn_with_permission_profile_context( + &self, + prompt: &str, + approval_policy: AskForApproval, + permission_profile: PermissionProfile, + service_tier: Option>, + environments: Option>, + ) -> Result<()> { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(self.config.cwd.as_path()) + .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + self.submit_turn_with_context( + prompt, + approval_policy, + sandbox_policy, + Some(permission_profile), + service_tier, + environments, + ) + .await + } + async fn submit_turn_with_context( &self, prompt: &str, approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, + permission_profile: Option, service_tier: Option>, environments: Option>, ) -> Result<()> { @@ -675,7 +725,7 @@ impl TestCodex { approval_policy, approvals_reviewer: None, sandbox_policy, - permission_profile: None, + permission_profile, model: session_model, effort: None, summary: None, @@ -835,6 +885,16 @@ impl TestCodexHarness { .await } + pub async fn submit_with_permission_profile( + &self, + prompt: &str, + permission_profile: PermissionProfile, + ) -> Result<()> { + self.test + .submit_turn_with_permission_profile(prompt, permission_profile) + .await + } + pub async fn request_bodies(&self) -> Vec { let path_matcher = path_regex(".*/responses$"); self.server diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index 3bfa264d79..ac49b5334b 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -8,8 +8,8 @@ use anyhow::Result; use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::DOCUMENT_EXTRACT_TEXT_RESOURCE_URI; use core_test_support::responses::ev_assistant_message; @@ -169,10 +169,10 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res let test = builder.build(&server).await?; tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Extract the report text with the app tool.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 859f537110..b4edf24668 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -12,11 +12,11 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpInvocation; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI; @@ -157,10 +157,10 @@ async fn search_tool_enabled_by_default_adds_tool_search() -> Result<()> { let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -221,10 +221,10 @@ async fn always_defer_feature_hides_small_app_tool_sets() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -265,10 +265,10 @@ async fn tool_search_disabled_exposes_apps_tools_directly() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -311,10 +311,10 @@ async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> { .with_config(move |config| configure_apps(config, apps_server.chatgpt_base_url.as_str())); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -347,10 +347,10 @@ async fn search_tool_adds_discovery_instructions_to_tool_description() -> Result let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -389,10 +389,10 @@ async fn search_tool_hides_apps_tools_without_search() -> Result<()> { let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "hello tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -425,10 +425,10 @@ async fn explicit_app_mentions_expose_apps_tools_without_search() -> Result<()> let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Use [$calendar](app://calendar) and then call tools.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -978,10 +978,10 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "Find the rmcp echo and image tools.", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 446c5ea08f..ed474114ff 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -2,7 +2,7 @@ #![allow(clippy::expect_used)] use anyhow::Result; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -136,9 +136,9 @@ async fn shell_output_stays_json_without_freeform_apply_patch( let responses = shell_responses(call_id, vec!["/bin/echo", "shell json"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the json shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -192,9 +192,9 @@ async fn shell_output_is_structured_with_freeform_apply_patch( let responses = shell_responses(call_id, vec!["/bin/echo", "freeform shell"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the structured shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -249,9 +249,9 @@ async fn shell_output_preserves_fixture_json_without_serialization( )?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "read the fixture JSON with sed", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -317,9 +317,9 @@ async fn shell_output_structures_fixture_with_serialization( )?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "read the fixture JSON with structured output", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -372,9 +372,9 @@ async fn shell_output_for_freeform_tool_records_duration( let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "sleep 0.2"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the structured shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -429,9 +429,9 @@ async fn shell_output_reserializes_truncated_content(output_type: ShellModelOutp let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "seq 1 400"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the truncation shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -495,9 +495,9 @@ async fn apply_patch_custom_tool_output_is_structured( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -537,9 +537,9 @@ async fn apply_patch_custom_tool_call_creates_file( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool to create a file", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -593,9 +593,9 @@ async fn apply_patch_custom_tool_call_updates_existing_file( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via custom tool to update a file", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -645,9 +645,9 @@ async fn apply_patch_custom_tool_call_reports_failure_output( harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "attempt a failing apply_patch via custom tool", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -688,9 +688,9 @@ async fn apply_patch_function_call_output_is_structured( .await; harness .test() - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "apply the patch via function-call apply_patch", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -727,9 +727,9 @@ async fn shell_output_is_structured_for_nonzero_exit(output_type: ShellModelOutp let responses = shell_responses(call_id, vec!["/bin/sh", "-c", "exit 42"], output_type)?; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the failing shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -778,9 +778,9 @@ async fn shell_command_output_is_freeform() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -830,9 +830,9 @@ async fn shell_command_output_is_not_truncated_under_10k_bytes() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -881,9 +881,9 @@ async fn shell_command_output_is_not_truncated_over_10k_bytes() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the shell_command script in the user's shell", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -929,9 +929,9 @@ async fn local_shell_call_output_is_structured() -> Result<()> { ]; let mock = mount_sse_sequence(&server, responses).await; - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "run the local shell command", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 30da83e9eb..b5b3752e0c 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -8,8 +8,8 @@ use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; use codex_models_manager::bundled_models_response; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -111,10 +111,10 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 5e522c0aa0..0fd072a70f 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -5,10 +5,10 @@ use anyhow::Context; use anyhow::Result; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses; @@ -82,7 +82,10 @@ async fn tool_call_output_configured_limit_chars_type() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -156,7 +159,10 @@ async fn tool_call_output_exceeds_limit_truncated_chars_limit() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -229,7 +235,10 @@ async fn tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; // Inspect what we sent back to the model; it should contain a truncated @@ -303,7 +312,10 @@ async fn tool_call_output_truncated_only_once() -> Result<()> { .await; fixture - .submit_turn_with_policy("trigger big shell output", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile( + "trigger big shell output", + PermissionProfile::Disabled, + ) .await?; let output = mock2 @@ -399,9 +411,9 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> let fixture = builder.build(&server).await?; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "call the rmcp echo tool with a very large message", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -496,6 +508,8 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { }); let fixture = builder.build(&server).await?; let session_model = fixture.session_configured.model.clone(); + let permission_profile = PermissionProfile::read_only(); + let sandbox_policy = permission_profile.to_legacy_sandbox_policy(fixture.cwd.path())?; fixture .codex @@ -509,8 +523,8 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile: Some(permission_profile), model: session_model, effort: None, summary: None, @@ -577,7 +591,7 @@ async fn token_policy_marker_reports_tokens() -> Result<()> { .await; fixture - .submit_turn_with_policy("run the shell tool", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile("run the shell tool", PermissionProfile::Disabled) .await?; let output = done_mock @@ -628,7 +642,7 @@ async fn byte_policy_marker_reports_bytes() -> Result<()> { .await; fixture - .submit_turn_with_policy("run the shell tool", SandboxPolicy::DangerFullAccess) + .submit_turn_with_permission_profile("run the shell tool", PermissionProfile::Disabled) .await?; let output = done_mock @@ -680,9 +694,9 @@ async fn shell_command_output_not_truncated_with_custom_limit() -> Result<()> { .await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "run big output without truncation", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -777,9 +791,9 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { let fixture = builder.build(&server).await?; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "call the rmcp echo tool with a very large message", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; diff --git a/codex-rs/core/tests/suite/web_search.rs b/codex-rs/core/tests/suite/web_search.rs index d3b0f21930..065711a371 100644 --- a/codex-rs/core/tests/suite/web_search.rs +++ b/codex-rs/core/tests/suite/web_search.rs @@ -2,7 +2,7 @@ use codex_features::Feature; use codex_protocol::config_types::WebSearchMode; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use core_test_support::responses; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; @@ -44,9 +44,9 @@ async fn web_search_mode_cached_sets_external_web_access_false() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello cached web search", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -86,9 +86,9 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello cached+live flags", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -132,9 +132,9 @@ async fn web_search_mode_defaults_to_cached_when_features_disabled() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello default cached web search", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await .expect("submit turn"); @@ -149,7 +149,7 @@ async fn web_search_mode_defaults_to_cached_when_features_disabled() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_updates_between_turns_with_sandbox_policy() { +async fn web_search_mode_updates_between_turns_with_permission_profile() { skip_if_no_network!(); let server = start_mock_server().await; @@ -187,10 +187,10 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello cached", SandboxPolicy::new_read_only_policy()) + test.submit_turn_with_permission_profile("hello cached", PermissionProfile::read_only()) .await .expect("submit first turn"); - test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess) + test.submit_turn_with_permission_profile("hello live", PermissionProfile::Disabled) .await .expect("submit second turn"); @@ -248,9 +248,9 @@ location = { country = "US", city = "New York", timezone = "America/New_York" } .await .expect("create test Codex conversation"); - test.submit_turn_with_policy( + test.submit_turn_with_permission_profile( "hello configured web search", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await .expect("submit turn"); diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index f685120132..2d2f9a41c1 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -23,6 +23,8 @@ use codex_protocol::user_input::UserInput; use serde::Serialize; use serde_json::Value; +use crate::permission_compat::legacy_compatible_permission_profile; + #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct AppCommand(Op); @@ -148,19 +150,12 @@ impl AppCommand { collaboration_mode: Option, personality: Option, ) -> Self { - let sandbox_policy = permission_profile + let legacy_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let sandbox_policy = legacy_profile .to_legacy_sandbox_policy(cwd.as_path()) .unwrap_or_else(|err| { - tracing::warn!( - %err, - "permission profile cannot be projected to legacy UserTurn sandbox; using read-only compatibility fallback" - ); - match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - unreachable!("read-only permissions must be legacy-compatible: {err}") - } - } + unreachable!("legacy-compatible permissions must project to legacy policy: {err}") }); Self(Op::UserTurn { items, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index e9a5fb447f..fb8f80267f 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3,6 +3,7 @@ use crate::bottom_pane::FeedbackAudience; use crate::legacy_core::append_message_history_entry; use crate::legacy_core::config::Config; use crate::legacy_core::message_history_metadata; +use crate::permission_compat::legacy_compatible_permission_profile; use crate::status::StatusAccountDisplay; use crate::status::plan_type_display_name; use codex_app_server_client::AppServerClient; @@ -542,21 +543,15 @@ impl AppServerSession { ) -> Result { let request_id = self.next_request_id(); let sandbox_policy = if matches!(self.thread_params_mode(), ThreadParamsMode::Remote) { - let policy = match permission_profile.to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - tracing::warn!( - %err, - "permission profile cannot be projected for remote turn/start; using read-only compatibility fallback" - ); - match PermissionProfile::read_only().to_legacy_sandbox_policy(cwd.as_path()) { - Ok(policy) => policy, - Err(err) => { - unreachable!("read-only permissions must be legacy-compatible: {err}") - } - } - } - }; + let legacy_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let policy = legacy_profile + .to_legacy_sandbox_policy(cwd.as_path()) + .unwrap_or_else(|err| { + unreachable!( + "legacy-compatible permissions must project to legacy policy: {err}" + ) + }); Some(policy.into()) } else { None @@ -1123,7 +1118,10 @@ fn sandbox_mode_from_permission_profile( .network_sandbox_policy() .is_enabled() .then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess) - } else if file_system_policy.can_write_path_with_cwd(cwd, cwd) { + } else if !file_system_policy + .get_writable_roots_with_cwd(cwd) + .is_empty() + { Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) } else { Some(codex_app_server_protocol::SandboxMode::ReadOnly) @@ -1513,6 +1511,12 @@ mod tests { use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; + use codex_protocol::models::ManagedFileSystemPermissions; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; @@ -1605,6 +1609,35 @@ mod tests { assert_eq!(fork.permission_profile, None); } + #[test] + fn sandbox_mode_preserves_non_cwd_write_roots_for_remote_sessions() { + let cwd = test_path_buf("/workspace/project").abs(); + let extra_root = test_path_buf("/workspace/cache").abs(); + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: extra_root }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + + assert_eq!( + sandbox_mode_from_permission_profile(&permission_profile, cwd.as_path()), + Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) + ); + } + #[tokio::test] async fn thread_lifecycle_params_forward_explicit_remote_cwd_override_for_remote_sessions() { let temp_dir = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 977fcc4b6d..bf265b69ef 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -150,6 +150,7 @@ mod npm_registry; pub(crate) mod onboarding; mod oss_selection; mod pager_overlay; +mod permission_compat; pub(crate) mod public_widgets; mod render; mod resize_reflow_cap; diff --git a/codex-rs/tui/src/permission_compat.rs b/codex-rs/tui/src/permission_compat.rs new file mode 100644 index 0000000000..2debbd2745 --- /dev/null +++ b/codex-rs/tui/src/permission_compat.rs @@ -0,0 +1,102 @@ +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::path::Path; + +pub(crate) fn legacy_compatible_permission_profile( + permission_profile: &PermissionProfile, + cwd: &Path, +) -> PermissionProfile { + if permission_profile.to_legacy_sandbox_policy(cwd).is_ok() { + return permission_profile.clone(); + } + + let file_system_policy = permission_profile.file_system_sandbox_policy(); + compatibility_workspace_write_profile( + &file_system_policy, + permission_profile.network_sandbox_policy(), + cwd, + ) +} + +fn compatibility_workspace_write_profile( + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: &Path, +) -> PermissionProfile { + let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); + let writable_roots = file_system_policy + .get_writable_roots_with_cwd(cwd) + .into_iter() + .map(|root| root.root) + .filter(|root| cwd_abs.as_ref() != Some(root)) + .collect::>(); + let tmpdir_writable = std::env::var_os("TMPDIR") + .filter(|tmpdir| !tmpdir.is_empty()) + .and_then(|tmpdir| { + AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok() + }) + .is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd)); + let slash_tmp = Path::new("/tmp"); + let slash_tmp_writable = slash_tmp.is_absolute() + && slash_tmp.is_dir() + && file_system_policy.can_write_path_with_cwd(slash_tmp, cwd); + + PermissionProfile::workspace_write_with( + &writable_roots, + network_policy, + !tmpdir_writable, + !slash_tmp_writable, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_protocol::models::ManagedFileSystemPermissions; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; + use pretty_assertions::assert_eq; + + #[test] + fn compatibility_profile_preserves_unbridgeable_write_roots() { + let cwd = AbsolutePathBuf::try_from("/workspace/project").expect("absolute cwd"); + let extra_root = AbsolutePathBuf::try_from("/workspace/extra").expect("absolute root"); + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: extra_root.clone(), + }, + access: FileSystemAccessMode::Write, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + + let compatibility_profile = + legacy_compatible_permission_profile(&permission_profile, cwd.as_path()); + let policy = compatibility_profile + .to_legacy_sandbox_policy(cwd.as_path()) + .expect("compatibility profile should project to legacy policy"); + let roots = policy + .get_writable_roots_with_cwd(cwd.as_path()) + .into_iter() + .map(|root| root.root) + .collect::>(); + + assert_eq!(roots, vec![extra_root, cwd]); + } +} From 01000fd841942bcd84ff6ffa245d074ec140811b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 03/20] core tests: build user turns from permission profiles --- codex-rs/core/tests/common/test_codex.rs | 19 +++++++++++++++---- codex-rs/core/tests/suite/image_rollout.rs | 15 ++++++++++----- codex-rs/core/tests/suite/json_result.rs | 9 ++++++--- codex-rs/core/tests/suite/live_reload.rs | 9 ++++++--- codex-rs/core/tests/suite/models_cache_ttl.rs | 9 ++++++--- codex-rs/core/tests/suite/pending_input.rs | 9 ++++++--- .../core/tests/suite/request_user_input.rs | 15 ++++++++++----- codex-rs/core/tests/suite/skills.rs | 9 ++++++--- codex-rs/core/tests/suite/tool_parallelism.rs | 15 ++++++++++----- .../core/tests/suite/websocket_fallback.rs | 9 ++++++--- 10 files changed, 81 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index fd92d0fde1..57d50ebf69 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -203,6 +203,18 @@ pub enum ShellModelOutput { // UnifiedExec has its own set of tests } +/// Returns the permission fields required by `Op::UserTurn` for tests that +/// construct the op directly. +pub fn turn_permission_fields( + permission_profile: PermissionProfile, + cwd: &Path, +) -> (SandboxPolicy, Option) { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(cwd) + .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + (sandbox_policy, Some(permission_profile)) +} + pub struct TestCodexBuilder { config_mutators: Vec>, auth: CodexAuth, @@ -689,14 +701,13 @@ impl TestCodex { service_tier: Option>, environments: Option>, ) -> Result<()> { - let sandbox_policy = permission_profile - .to_legacy_sandbox_policy(self.config.cwd.as_path()) - .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); + let (sandbox_policy, permission_profile) = + turn_permission_fields(permission_profile, self.config.cwd.as_path()); self.submit_turn_with_context( prompt, approval_policy, sandbox_policy, - Some(permission_profile), + permission_profile, service_tier, environments, ) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 18ebe0fb03..f819e1cfdf 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -1,13 +1,13 @@ use anyhow::Context; use codex_protocol::models::ContentItem; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -18,6 +18,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use image::ImageBuffer; use image::Rgba; @@ -108,6 +109,8 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu responses::mount_sse_once(&server, response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -125,8 +128,8 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -196,6 +199,8 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> responses::mount_sse_once(&server, response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -213,8 +218,8 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 01783a365d..c4ce8ac908 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -1,14 +1,15 @@ #![cfg(not(target_os = "windows"))] +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use responses::ev_assistant_message; @@ -69,6 +70,8 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { responses::mount_sse_once_match(&server, match_json_text_param, sse1).await; let TestCodex { codex, cwd, .. } = test_codex().build(&server).await?; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); // 1) Normal user input – should hit server once. codex @@ -82,8 +85,8 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 25206a6fd3..c422073e4f 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -8,10 +8,10 @@ use std::time::Duration; use anyhow::Result; use codex_config::config_toml::ProjectConfig; use codex_protocol::config_types::TrustLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ResponsesRequest; @@ -19,6 +19,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use tokio::time::timeout; @@ -46,6 +47,8 @@ fn contains_skill_body(request: &ResponsesRequest, skill_body: &str) -> bool { async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) -> Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -63,8 +66,8 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 8eb8fca8ac..e2688afc97 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -9,6 +9,7 @@ use codex_login::CodexAuth; use codex_models_manager::client_version_to_whole; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelVisibility; @@ -19,7 +20,6 @@ use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -28,6 +28,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde::Deserialize; @@ -87,6 +88,8 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { sse_response(response_body).insert_header("X-Models-Etag", ETAG), ) .await; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); codex .submit(Op::UserTurn { @@ -99,8 +102,8 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { cwd: test.cwd_path().to_path_buf(), approval_policy: codex_protocol::protocol::AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index cfe9ba8fcc..b582a2abc7 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use codex_core::CodexThread; use codex_protocol::AgentPath; use codex_protocol::items::TurnItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; @@ -25,6 +25,7 @@ use core_test_support::streaming_sse::StreamingSseServer; use core_test_support::streaming_sse::start_streaming_sse_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; @@ -108,6 +109,8 @@ async fn submit_user_input(codex: &CodexThread, text: &str) { } async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -119,8 +122,8 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { cwd: test.config.cwd.to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 7814f4936c..c82a5a1672 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -6,10 +6,10 @@ use codex_features::Feature; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -24,6 +24,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -128,6 +129,8 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -140,8 +143,8 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -248,6 +251,8 @@ where let session_model = session_configured.model.clone(); let collaboration_mode = build_mode(session_model.clone()); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -260,8 +265,8 @@ where cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 1edaefb783..ab5e462991 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -9,9 +9,9 @@ use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::ExecutorFileSystem; use codex_login::CodexAuth; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; @@ -24,6 +24,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -97,6 +98,8 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { .await; let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -114,8 +117,8 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { cwd: test.config.cwd.to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index a0ee7b1140..47282c283d 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -5,10 +5,10 @@ use std::fs; use std::time::Duration; use std::time::Instant; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -24,6 +24,7 @@ use core_test_support::streaming_sse::StreamingSseChunk; use core_test_support::streaming_sse::start_streaming_sse_server; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; @@ -32,6 +33,8 @@ use tokio::sync::oneshot; async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd.path()); test.codex .submit(Op::UserTurn { @@ -44,8 +47,8 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { cwd: test.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -352,6 +355,8 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an .await?; let session_model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, test.cwd.path()); test.codex .submit(Op::UserTurn { environments: None, @@ -363,8 +368,8 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an cwd: test.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index dbe2e3c6d7..f073ce7ab8 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -1,9 +1,9 @@ use anyhow::Result; use codex_model_provider_info::WireApi; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_completed; @@ -14,6 +14,7 @@ use core_test_support::responses::sse; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use tokio::time::Duration; use tokio::time::timeout; @@ -147,6 +148,8 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result cwd, .. } = builder.build(&server).await?; + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.path()); codex .submit(Op::UserTurn { @@ -159,8 +162,8 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_configured.model.clone(), effort: None, summary: None, From 820a69987a6db9deb49233f97407cc224054809f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 04/20] core tests: migrate more turns to permission profiles --- codex-rs/core/tests/suite/exec.rs | 6 +-- .../core/tests/suite/models_etag_responses.rs | 12 +++-- codex-rs/core/tests/suite/shell_snapshot.rs | 27 +++++++---- codex-rs/core/tests/suite/sqlite_state.rs | 12 +++-- codex-rs/core/tests/suite/tool_harness.rs | 48 ++++++++++++------- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index a0dd7f8e25..923d223e13 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -9,7 +9,6 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::Result; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use core_test_support::PathExt; @@ -50,12 +49,9 @@ where arg0: None, }; - let policy = SandboxPolicy::new_read_only_policy(); - let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&policy); - process_exec_tool_call( params, - &permission_profile, + &PermissionProfile::read_only(), &cwd, &None, /*use_legacy_landlock*/ false, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 0551dcf8e1..a9aa843ad1 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -6,11 +6,11 @@ use std::time::Duration; use anyhow::Result; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -21,6 +21,7 @@ use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event_with_timeout; use pretty_assertions::assert_eq; use wiremock::MockServer; @@ -61,6 +62,9 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch let codex = Arc::clone(&test.codex); let cwd = Arc::clone(&test.cwd); let session_model = test.session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); assert_eq!(spawn_models_mock.requests().len(), 1); assert_eq!(spawn_models_mock.single_request_path(), "/v1/models"); @@ -107,11 +111,11 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 1204f2b078..f757fa322a 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -1,11 +1,11 @@ use anyhow::Result; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -15,6 +15,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -154,6 +155,8 @@ async fn run_snapshot_command_with_options( let codex_home = test.home.path().to_path_buf(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { @@ -166,8 +169,8 @@ async fn run_snapshot_command_with_options( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -247,6 +250,8 @@ async fn run_shell_command_snapshot_with_options( let codex_home = test.home.path().to_path_buf(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { @@ -259,8 +264,8 @@ async fn run_shell_command_snapshot_with_options( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -321,6 +326,8 @@ async fn run_tool_turn_on_harness( let codex = test.codex.clone(); let session_model = test.session_configured.model.clone(); let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { environments: None, @@ -332,8 +339,8 @@ async fn run_tool_turn_on_harness( cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -558,6 +565,8 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { mount_sse_sequence(harness.server(), responses).await; let model = test.session_configured.model.clone(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); codex .submit(Op::UserTurn { environments: None, @@ -569,8 +578,8 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { cwd: cwd.clone(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 8ad9ede5b9..8250f5493d 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -4,12 +4,12 @@ use codex_config::types::McpServerTransportConfig; use codex_features::Feature; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; @@ -26,6 +26,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -396,6 +397,9 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< let test = builder.build(&server).await?; let db = test.codex.state_db().expect("state db enabled"); let thread_id = test.session_configured.session_id; + let cwd = test.cwd_path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd.as_path()); test.codex .submit(Op::UserTurn { @@ -405,11 +409,11 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index db93c0b66e..62d6dcef90 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -4,11 +4,11 @@ use std::fs; use assert_matches::assert_matches; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::plan_tool::StepStatus; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses; @@ -24,6 +24,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use serde_json::Value; use serde_json::json; @@ -75,6 +76,9 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -84,11 +88,11 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -148,6 +152,9 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -157,11 +164,11 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -231,6 +238,9 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -240,11 +250,11 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -329,6 +339,9 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -338,11 +351,11 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, @@ -435,6 +448,9 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { let second_mock = responses::mount_sse_once(&server, second_response).await; let session_model = session_configured.model.clone(); + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { @@ -444,11 +460,11 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, From 7fc48fdb693a2c8ad1347878f0d78bf7c680659c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 05/20] core tests: configure profiles directly --- codex-rs/core/tests/suite/code_mode.rs | 13 ++++-- codex-rs/core/tests/suite/codex_delegate.rs | 12 +++--- .../core/tests/suite/model_visible_layout.rs | 40 +++++++++++++------ codex-rs/core/tests/suite/otel.rs | 7 ++-- codex-rs/core/tests/suite/tools.rs | 38 +++++++++--------- codex-rs/core/tests/suite/user_shell_cmd.rs | 16 +++++--- 6 files changed, 78 insertions(+), 48 deletions(-) diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 413fd90bbb..af94252c02 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -11,10 +11,10 @@ use codex_models_manager::bundled_models_response; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::assert_regex_match; @@ -30,6 +30,7 @@ use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -2607,6 +2608,10 @@ text( ) .await; + let cwd = test.cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + test.codex .submit(Op::UserTurn { environments: None, @@ -2615,11 +2620,11 @@ text( text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd.path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: test.session_configured.model.clone(), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index 461b07284c..2ccb6fecba 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -1,12 +1,12 @@ use codex_core::config::Constrained; use codex_core::sandboxing::SandboxPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::responses::ev_apply_patch_function_call; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -65,8 +65,9 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config - .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::read_only()) + .expect("set permission profile"); }); let test = builder.build(&server).await.expect("build test codex"); @@ -149,8 +150,9 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); // Use a restricted sandbox so patch approval is required config - .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::read_only()) + .expect("set permission profile"); config.include_apply_patch_tool = true; }); let test = builder.build(&server).await.expect("build test codex"); diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 2f06d5af21..7ae148788e 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -6,10 +6,10 @@ use std::sync::Arc; use anyhow::Result; use codex_config::types::Personality; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; @@ -24,6 +24,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use serde_json::json; @@ -111,6 +112,9 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { let test = builder.build(&server).await?; let preturn_context_diff_cwd = test.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD); fs::create_dir_all(&preturn_context_diff_cwd)?; + let first_turn_cwd = test.cwd_path().to_path_buf(); + let (first_sandbox_policy, first_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), first_turn_cwd.as_path()); test.codex .submit(Op::UserTurn { @@ -120,11 +124,11 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), + cwd: first_turn_cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: first_sandbox_policy, + permission_profile: first_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -138,6 +142,10 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { }) .await; + let (second_sandbox_policy, second_permission_profile) = turn_permission_fields( + PermissionProfile::read_only(), + preturn_context_diff_cwd.as_path(), + ); test.codex .submit(Op::UserTurn { environments: None, @@ -149,8 +157,8 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { cwd: preturn_context_diff_cwd, approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: second_sandbox_policy, + permission_profile: second_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -218,6 +226,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd_two.join("AGENTS.md"), "# AGENTS two\n\n\nTurn two agents instructions.\n\n", )?; + let (first_sandbox_policy, first_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd_one.as_path()); test.codex .submit(Op::UserTurn { @@ -230,8 +240,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd: cwd_one.clone(), approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: first_sandbox_policy, + permission_profile: first_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -245,6 +255,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R }) .await; + let (second_sandbox_policy, second_permission_profile) = + turn_permission_fields(PermissionProfile::read_only(), cwd_two.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -256,8 +268,8 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R cwd: cwd_two, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy: second_sandbox_policy, + permission_profile: second_permission_profile, model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: None, @@ -358,6 +370,10 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul let resumed = resume_builder.resume(&server, home, rollout_path).await?; let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD); fs::create_dir_all(&resume_override_cwd)?; + let (sandbox_policy, permission_profile) = turn_permission_fields( + PermissionProfile::read_only(), + resume_override_cwd.as_path(), + ); resumed .codex .submit(Op::UserTurn { @@ -370,8 +386,8 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul cwd: resume_override_cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + sandbox_policy, + permission_profile, model: resumed.session_configured.model.clone(), effort: resumed.config.model_reasoning_effort, summary: None, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 3d2f5102e2..5bf41bd547 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -1,10 +1,10 @@ use codex_core::config::Constrained; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -1111,8 +1111,9 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { .with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index aff8755b11..46195e4dd0 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -190,10 +190,10 @@ async fn custom_tool_unknown_returns_custom_output_error() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "invoke custom tool", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -408,10 +408,10 @@ async fn shell_escalated_permissions_rejected_then_ok() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run the shell command", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -488,9 +488,9 @@ async fn sandbox_denied_shell_returns_original_output() -> Result<()> { let mock = mount_sse_sequence(&server, responses).await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "run a command that should be denied by the read-only sandbox", - SandboxPolicy::new_read_only_policy(), + PermissionProfile::read_only(), ) .await?; @@ -675,10 +675,10 @@ async fn collect_tools(use_unified_exec: bool) -> Result> { }); let test = builder.build(&server).await?; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "list tools", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -746,10 +746,10 @@ async fn shell_timeout_includes_timeout_prefix_and_metadata() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run a long command", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; @@ -791,8 +791,9 @@ async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + .permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }); let test = builder.build(&server).await?; @@ -837,10 +838,10 @@ time.sleep(60) let start = Instant::now(); let output_str = tokio::time::timeout(Duration::from_secs(10), async { - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "run a command with a detached grandchild", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; let timeout_item = second_mock.single_request().function_call_output(call_id); @@ -885,8 +886,9 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(|cfg| { - cfg.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + cfg.permissions + .set_permission_profile(PermissionProfile::Disabled) + .expect("set permission profile"); }); let test = builder.build(&server).await?; @@ -922,10 +924,10 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { ) .await; - test.submit_turn_with_policies( + test.submit_turn_with_approval_and_permission_profile( "spawn a missing binary", AskForApproval::Never, - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 5526356693..1285b9f925 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -8,7 +8,6 @@ use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ExecOutputStream; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::user_input::UserInput; use core_test_support::PathBufExt; @@ -23,6 +22,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use core_test_support::wait_for_event_with_timeout; @@ -168,6 +168,10 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> ]); let mock = responses::mount_sse_sequence(&server, vec![first, second]).await; + let cwd = fixture.cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); + fixture .codex .submit(Op::UserTurn { @@ -177,11 +181,11 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: fixture.cwd.path().to_path_buf(), + cwd, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: fixture.session_configured.model.clone(), effort: None, summary: None, @@ -484,9 +488,9 @@ async fn user_shell_command_is_truncated_only_once() -> anyhow::Result<()> { .await; fixture - .submit_turn_with_policy( + .submit_turn_with_permission_profile( "trigger big shell_command output", - SandboxPolicy::DangerFullAccess, + PermissionProfile::Disabled, ) .await?; From 087d70b4da4cf071ce9fe6855f3bbddef2773abe Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 06/20] core tests: send model turns with permission profiles --- codex-rs/core/tests/suite/remote_models.rs | 21 ++++++++++++------- .../suite/responses_api_proxy_headers.rs | 17 +++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index a69dae4a5b..49218c78d2 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -8,6 +8,7 @@ use codex_models_manager::bundled_models_response; use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::SharedModelsManager; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; @@ -21,7 +22,6 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; use core_test_support::responses::ev_assistant_message; @@ -37,6 +37,7 @@ use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; @@ -627,6 +628,9 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { ]; mount_sse_sequence(&server, responses).await; + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { items: vec![UserInput::Text { @@ -634,11 +638,11 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: REMOTE_MODEL_SLUG.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), @@ -855,6 +859,9 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { }) .await?; + let cwd_path = cwd.path().to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); codex .submit(Op::UserTurn { items: vec![UserInput::Text { @@ -862,11 +869,11 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: cwd.path().to_path_buf(), + cwd: cwd_path, approval_policy: AskForApproval::Never, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::DangerFullAccess, - permission_profile: None, + sandbox_policy, + permission_profile, model: model.to_string(), effort: None, summary: Some(ReasoningSummary::Auto), diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index cd9a73696d..09e99f78c3 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -4,10 +4,10 @@ use anyhow::Result; use anyhow::anyhow; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponsesRequest; @@ -21,6 +21,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; +use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; use serde_json::json; use std::time::Duration; @@ -125,6 +126,9 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() - async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> { let session_model = test.session_configured.model.clone(); + let cwd = test.config.cwd.to_path_buf(); + let (sandbox_policy, permission_profile) = + turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path()); test.codex .submit(Op::UserTurn { environments: None, @@ -133,16 +137,11 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, - cwd: test.config.cwd.to_path_buf(), + cwd, approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, - sandbox_policy: SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - permission_profile: None, + sandbox_policy, + permission_profile, model: session_model, effort: None, summary: None, From 09b75a2dcbef0ee273f3bb081c01f7665cdb69ff Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 07/20] core tests: migrate model/personality turns to profiles --- codex-rs/core/tests/suite/model_switching.rs | 319 +++++----------- codex-rs/core/tests/suite/personality.rs | 370 ++++++------------- 2 files changed, 216 insertions(+), 473 deletions(-) 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/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; From 375aad0d823a5826cb333cee8e786f1f435ba165 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 08/20] core tests: migrate view image turns to profiles --- codex-rs/core/tests/suite/view_image.rs | 287 +++++++----------------- 1 file changed, 84 insertions(+), 203 deletions(-) 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( From 11faa1d8e710f970a341cbf3dd76399dc26e8a30 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 09/20] core tests: migrate safety check turns to profiles --- .../tests/suite/safety_check_downgrade.rs | 171 ++++-------------- 1 file changed, 37 insertions(+), 134 deletions(-) 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; From 18d6b4f8e543a6731fa9cafdf6c98f7a2c8ae768 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 10/20] core tests: migrate plan item turns to profiles --- codex-rs/core/tests/suite/items.rs | 152 +++++++++++------------------ 1 file changed, 57 insertions(+), 95 deletions(-) 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; From bb289b0bc8269f092c1087c2d215e9fa92a18330 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 11/20] core tests: migrate tools tests to permission profiles --- codex-rs/core/tests/suite/tools.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 From d5db90a854e8b5e4dfa8a87c687d38626c3b7489 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 12/20] core tests: migrate permissions message tests to profiles --- .../core/tests/suite/permissions_messages.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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, From 0031063584cc8f2311343f04629ea7025d11e81e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 13/20] core tests: migrate exec policy turns to profiles --- codex-rs/core/tests/suite/exec_policy.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) 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?; From 37ad5dbb992ec097aa25fb38becf68f5e5c1a863 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 14/20] core tests: migrate prompt caching turns to profiles --- codex-rs/core/tests/suite/prompt_caching.rs | 49 ++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) 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), From 54074de65d26fe3ab3c58ec5a50e4fc70380d942 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 15/20] core tests: migrate request permissions tool turns to profiles --- .../tests/suite/request_permissions_tool.rs | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) 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?; From b85ae71a5075c46eb66208902b13e261838b0082 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 16/20] core tests: migrate zsh-fork permissions to profiles --- codex-rs/core/tests/common/zsh_fork.rs | 28 ++++++++------- codex-rs/core/tests/suite/approvals.rs | 38 +++++++++++++++------ codex-rs/core/tests/suite/skill_approval.rs | 23 +++++++------ 3 files changed, 56 insertions(+), 33 deletions(-) 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/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/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?; From b46b36a11f95854aade9403b48dc2ec4ca07fd0e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 17/20] core tests: migrate compact turns to profiles --- codex-rs/core/tests/suite/compact.rs | 195 +++++++++------------------ 1 file changed, 61 insertions(+), 134 deletions(-) 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| { From bb764dc4503fa37dec6c656700a52f819343b4b9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 18/20] core tests: migrate rmcp turns to profiles --- codex-rs/core/tests/suite/rmcp_client.rs | 322 +++++------------------ 1 file changed, 72 insertions(+), 250 deletions(-) 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. From b1026eec232102b1a1339f655d2cd1d33a401379 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 19/20] core tests: migrate apply patch turns to profiles --- codex-rs/core/tests/suite/apply_patch_cli.rs | 219 +++---------------- 1 file changed, 25 insertions(+), 194 deletions(-) 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( From adcadcd5058dd4080caa6dc3e6aefac903d73917 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 28 Apr 2026 11:24:40 -0700 Subject: [PATCH 20/20] core tests: migrate hook turns to profiles --- codex-rs/core/tests/suite/hooks.rs | 68 +++++++++++++++++------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 28185a0a5c..985d76344c 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?; @@ -1869,9 +1879,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?;