diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6a1e6fbae801..6e81704752e1 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -4235,9 +4235,13 @@ impl CodexMessageProcessor { thread_status, /*has_live_in_progress_turn*/ false, ); - let permission_profile = thread_response_permission_profile( - codex_thread.config_snapshot().await.permission_profile, + let config_snapshot = codex_thread.config_snapshot().await; + let sandbox = thread_response_sandbox_policy( + &config_snapshot.permission_profile, + config_snapshot.cwd.as_path(), ); + let permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile.clone()); let response = ThreadResumeResponse { thread, @@ -4248,7 +4252,7 @@ impl CodexMessageProcessor { instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), - sandbox: session_configured.sandbox_policy.into(), + sandbox, permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -4831,9 +4835,13 @@ impl CodexMessageProcessor { .await, /*has_in_progress_turn*/ false, ); - let permission_profile = thread_response_permission_profile( - forked_thread.config_snapshot().await.permission_profile, + let config_snapshot = forked_thread.config_snapshot().await; + let sandbox = thread_response_sandbox_policy( + &config_snapshot.permission_profile, + config_snapshot.cwd.as_path(), ); + let permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile); let response = ThreadForkResponse { thread: thread.clone(), @@ -4844,7 +4852,7 @@ impl CodexMessageProcessor { instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), - sandbox: session_configured.sandbox_policy.into(), + sandbox, permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -8091,13 +8099,14 @@ async fn handle_pending_thread_resume_request( service_tier, approval_policy, approvals_reviewer, - sandbox_policy, + sandbox_policy: _, permission_profile, cwd, reasoning_effort, .. } = pending.config_snapshot; let instruction_sources = pending.instruction_sources; + let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path()); let permission_profile = thread_response_permission_profile(permission_profile); let response = ThreadResumeResponse { @@ -8109,7 +8118,7 @@ async fn handle_pending_thread_resume_request( instruction_sources, approval_policy: approval_policy.into(), approvals_reviewer: approvals_reviewer.into(), - sandbox: sandbox_policy.into(), + sandbox, permission_profile, reasoning_effort, }; @@ -9240,6 +9249,20 @@ fn thread_response_permission_profile( Some(permission_profile.into()) } +fn thread_response_sandbox_policy( + permission_profile: &codex_protocol::models::PermissionProfile, + cwd: &Path, +) -> codex_app_server_protocol::SandboxPolicy { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( + permission_profile, + &file_system_policy, + permission_profile.network_sandbox_policy(), + cwd, + ); + sandbox_policy.into() +} + fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) -> bool { if matches!( overrides.sandbox_mode, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index d4c0d337cf68..176865ee6cf1 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -867,7 +867,6 @@ impl Session { // Dispatch the SessionConfiguredEvent first and then report any errors. // If resuming, include converted initial messages in the payload so UIs can render them immediately. let initial_messages = initial_history.get_event_msgs(); - let session_sandbox_policy = session_configuration.sandbox_policy(); let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -879,8 +878,7 @@ impl Session { service_tier: session_configuration.service_tier, approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, - sandbox_policy: session_sandbox_policy.clone(), - permission_profile: Some(session_configuration.permission_profile()), + permission_profile: session_configuration.permission_profile(), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), history_log_id, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 283220e8fa2c..c9416ac2b46a 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1527,17 +1527,12 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> let test = builder.build(&server).await?; - assert_eq!( - test.session_configured.sandbox_policy, - expected_sandbox_policy - ); let expected_permission_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &expected_sandbox_policy, ); assert_eq!( - test.session_configured.permission_profile, - Some(expected_permission_profile), + test.session_configured.permission_profile, expected_permission_profile, "ExternalSandbox is represented explicitly instead of as a lossy root-write profile" ); Ok(()) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 9e9d9bc148e1..fc3392db2fbf 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1046,8 +1046,9 @@ fn session_configured_from_thread_response( service_tier, approval_policy, approvals_reviewer, - sandbox_policy, - permission_profile, + permission_profile: permission_profile.unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd.as_path()) + }), cwd, reasoning_effort, history_log_id: 0, diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 3a7b5d0fcc55..23dcd6733afa 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -28,9 +28,9 @@ use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::WebSearchAction; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; @@ -114,8 +114,7 @@ fn session_configured_produces_thread_started_event() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 67ceb91dd819..78ef4cacffb8 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -231,10 +231,10 @@ mod tests { use anyhow::Result; use codex_protocol::ThreadId; + use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; @@ -304,8 +304,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -349,8 +348,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -389,9 +387,7 @@ mod tests { "model_provider_id": "test-provider", "approval_policy": "never", "approvals_reviewer": "user", - "sandbox_policy": { - "type": "read-only" - }, + "permission_profile": session_configured_event.permission_profile, "cwd": test_path_buf("/home/user/project"), "reasoning_effort": session_configured_event.reasoning_effort, "history_log_id": session_configured_event.history_log_id, @@ -419,8 +415,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -460,9 +455,7 @@ mod tests { "model_provider_id": "test-provider", "approval_policy": "never", "approvals_reviewer": "user", - "sandbox_policy": { - "type": "read-only" - }, + "permission_profile": session_configured_event.permission_profile, "cwd": test_path_buf("/home/user/project"), "reasoning_effort": session_configured_event.reasoning_effort, "history_log_id": session_configured_event.history_log_id, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 85ef863b81f0..c731dfae3a87 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3541,7 +3541,7 @@ pub struct SessionNetworkProxyRuntime { pub socks_addr: String, } -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +#[derive(Debug, Clone, Serialize, JsonSchema, TS)] pub struct SessionConfiguredEvent { pub session_id: ThreadId, #[serde(skip_serializing_if = "Option::is_none")] @@ -3569,16 +3569,8 @@ pub struct SessionConfiguredEvent { #[serde(default)] pub approvals_reviewer: ApprovalsReviewer, - /// Legacy sandbox projection for commands executed in the system. - /// - /// Consumers should prefer `permission_profile` when it is present. This - /// field remains available as a compatibility fallback for older emitters - /// and sessions that only expose legacy sandbox state. - pub sandbox_policy: SandboxPolicy, - /// Canonical effective permissions for commands executed in the session. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub permission_profile: Option, + pub permission_profile: PermissionProfile, /// Working directory that should be treated as the *root* of the /// session. @@ -3609,6 +3601,70 @@ pub struct SessionConfiguredEvent { pub rollout_path: Option, } +impl<'de> Deserialize<'de> for SessionConfiguredEvent { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Wire { + session_id: ThreadId, + forked_from_id: Option, + #[serde(default)] + thread_name: Option, + model: String, + model_provider_id: String, + service_tier: Option, + approval_policy: AskForApproval, + #[serde(default)] + approvals_reviewer: ApprovalsReviewer, + // `SessionConfiguredEvent` is persisted into rollout history. Older + // rollouts only have `sandbox_policy`, so accept it on deserialize + // and immediately project it into the canonical `permission_profile`. + sandbox_policy: Option, + permission_profile: Option, + cwd: AbsolutePathBuf, + reasoning_effort: Option, + history_log_id: u64, + history_entry_count: usize, + initial_messages: Option>, + network_proxy: Option, + rollout_path: Option, + } + + let wire = Wire::deserialize(deserializer)?; + let permission_profile = match (wire.permission_profile, wire.sandbox_policy) { + (Some(permission_profile), _) => permission_profile, + (None, Some(sandbox_policy)) => PermissionProfile::from_legacy_sandbox_policy_for_cwd( + &sandbox_policy, + wire.cwd.as_path(), + ), + (None, None) => { + return Err(serde::de::Error::missing_field("permission_profile")); + } + }; + + Ok(Self { + session_id: wire.session_id, + forked_from_id: wire.forked_from_id, + thread_name: wire.thread_name, + model: wire.model, + model_provider_id: wire.model_provider_id, + service_tier: wire.service_tier, + approval_policy: wire.approval_policy, + approvals_reviewer: wire.approvals_reviewer, + permission_profile, + cwd: wire.cwd, + reasoning_effort: wire.reasoning_effort, + history_log_id: wire.history_log_id, + history_entry_count: wire.history_entry_count, + initial_messages: wire.initial_messages, + network_proxy: wire.network_proxy, + rollout_path: wire.rollout_path, + }) + } +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ThreadNameUpdatedEvent { pub thread_id: ThreadId, @@ -5088,6 +5144,7 @@ mod tests { fn serialize_event() -> Result<()> { let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; let rollout_file = NamedTempFile::new()?; + let permission_profile = PermissionProfile::read_only(); let event = Event { id: "1234".to_string(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -5099,8 +5156,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: permission_profile.clone(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -5120,9 +5176,7 @@ mod tests { "model_provider_id": "openai", "approval_policy": "never", "approvals_reviewer": "user", - "sandbox_policy": { - "type": "read-only" - }, + "permission_profile": permission_profile, "cwd": test_path_buf("/home/user/project"), "reasoning_effort": "medium", "history_log_id": 0, @@ -5134,6 +5188,28 @@ mod tests { Ok(()) } + #[test] + fn deserialize_legacy_session_configured_event_uses_sandbox_policy() -> Result<()> { + let cwd = test_path_buf("/home/user/project"); + let value = json!({ + "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8", + "model": "codex-mini-latest", + "model_provider_id": "openai", + "approval_policy": "never", + "approvals_reviewer": "user", + "sandbox_policy": { + "type": "read-only" + }, + "cwd": cwd, + "history_log_id": 0, + "history_entry_count": 0, + }); + + let event: SessionConfiguredEvent = serde_json::from_value(value)?; + assert_eq!(event.permission_profile, PermissionProfile::read_only()); + Ok(()) + } + #[test] fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> { let event = ExecCommandOutputDeltaEvent { diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 9f6b63122636..316465537488 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -556,6 +556,7 @@ mod tests { use crate::app::test_support::app_enabled_in_effective_config; use crate::app::test_support::make_test_app; use crate::test_support::PathBufExt; + use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::SessionConfiguredEvent; @@ -653,8 +654,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: next_cwd.clone().abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 8a08f8bf165d..6c82dac63853 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3566,8 +3566,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::High), history_log_id: 0, @@ -4317,8 +4316,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4381,8 +4379,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4475,8 +4472,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4859,8 +4855,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4972,8 +4967,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index f69eea4afce8..2b1871886038 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -142,12 +142,8 @@ use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::UserMessageItem; use codex_protocol::models::MessagePhase; -use codex_protocol::models::PermissionProfile; -use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg; use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus; #[cfg(test)] @@ -1620,8 +1616,7 @@ fn thread_session_state_to_legacy_event( service_tier: session.service_tier, approval_policy: session.approval_policy, approvals_reviewer: session.approvals_reviewer, - sandbox_policy: session.sandbox_policy, - permission_profile: Some(session.permission_profile), + permission_profile: session.permission_profile, cwd: session.cwd, reasoning_effort: session.reasoning_effort, history_log_id: session.history_log_id, @@ -2349,32 +2344,14 @@ impl ChatWidget { self.config.permissions.approval_policy = Constrained::allow_only(event.approval_policy); } - let permission_sync = match event.permission_profile.clone() { - Some(permission_profile) => self - .config - .permissions - .set_permission_profile(permission_profile), - None => self - .config - .permissions - .set_legacy_sandbox_policy(event.sandbox_policy.clone(), event.cwd.as_path()), - }; + let permission_sync = self + .config + .permissions + .set_permission_profile(event.permission_profile.clone()); if let Err(err) = permission_sync { tracing::warn!(%err, "failed to sync permissions from SessionConfigured"); - let permission_profile = event.permission_profile.clone().unwrap_or_else(|| { - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &event.sandbox_policy, - event.cwd.as_path(), - ); - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&event.sandbox_policy), - &file_system_sandbox_policy, - NetworkSandboxPolicy::from(&event.sandbox_policy), - ) - }); self.config.permissions.permission_profile = - Constrained::allow_only(permission_profile); + Constrained::allow_only(event.permission_profile.clone()); } self.config.approvals_reviewer = event.approvals_reviewer; self.status_line_project_root_name_cache = None; diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 302c4ea38c1c..d21febaa8128 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -16,8 +16,7 @@ async fn submission_preserves_text_elements_and_local_images() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -121,8 +120,7 @@ async fn submission_includes_configured_permission_profile() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: Some(expected_permission_profile.clone()), + permission_profile: expected_permission_profile.clone(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -172,10 +170,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::ExternalSandbox { - network_access: codex_protocol::protocol::NetworkAccess::Restricted, - }, - permission_profile: Some(expected_permission_profile.clone()), + permission_profile: expected_permission_profile.clone(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -218,8 +213,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -314,8 +308,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -380,8 +373,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -421,8 +413,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -462,8 +453,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -506,8 +496,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 4d87914aa40a..ec1d03f9fc50 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1004,8 +1004,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 77f52524f47e..443cac0532d2 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -24,8 +24,7 @@ async fn resumed_initial_messages_render_history() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -138,8 +137,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -200,8 +198,7 @@ async fn replayed_user_message_preserves_remote_image_urls() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -256,7 +253,6 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .expect("set sandbox policy"); chat.config.cwd = test_path_buf("/home/user/main").abs(); - let legacy_fallback_sandbox = SandboxPolicy::new_read_only_policy(); let expected_cwd = test_path_buf("/home/user/sub-agent").abs(); let expected_file_system_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { @@ -289,8 +285,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: legacy_fallback_sandbox, - permission_profile: Some(expected_permission_profile.clone()), + permission_profile: expected_permission_profile.clone(), cwd: expected_cwd.clone(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -355,8 +350,9 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: expected_sandbox.clone(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, cwd: test_path_buf("/home/user/external").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -405,8 +401,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -459,8 +454,7 @@ async fn replayed_user_message_with_only_local_images_does_not_render_history_ce service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -781,8 +775,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, @@ -829,8 +822,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: codex_protocol::models::PermissionProfile::read_only(), cwd: test_project_path().abs(), reasoning_effort: None, 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 388bc67f81c3..4666e680490b 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -483,8 +483,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure service_tier: None, approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: None, + permission_profile: PermissionProfile::workspace_write(), cwd: test_project_path().abs(), reasoning_effort: None, history_log_id: 0, @@ -519,6 +518,13 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w .set_enabled(Feature::GuardianApproval, /*enabled*/ true); let extra_root = test_path_buf("/tmp/guardian-approvals-extra").abs(); + let cwd = test_project_path().abs(); + let permission_profile = PermissionProfile::workspace_write_with( + &[extra_root], + codex_protocol::protocol::NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); chat.handle_codex_event(Event { id: "session-configured-custom-workspace".to_string(), @@ -531,14 +537,8 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w service_tier: None, approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, - sandbox_policy: SandboxPolicy::WorkspaceWrite { - writable_roots: vec![extra_root], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - permission_profile: None, - cwd: test_project_path().abs(), + permission_profile, + cwd, reasoning_effort: None, history_log_id: 0, history_entry_count: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 65f6ae0c310d..c6e1f73763a7 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1059,8 +1059,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -1305,8 +1304,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index d7aefbe4b4fa..19890a5d3c44 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1741,8 +1741,7 @@ async fn session_configured_clears_goal_status_footer() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 33e2f25c637f..af77b6ecb853 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -54,6 +54,8 @@ use codex_protocol::account::PlanType; use codex_protocol::mcp::Resource; #[cfg(test)] use codex_protocol::mcp::ResourceTemplate; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::WebSearchAction; use codex_protocol::models::local_image_label_text; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -64,7 +66,6 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpInvocation; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputQuestion; @@ -1240,7 +1241,7 @@ pub(crate) fn new_session_info( model, reasoning_effort, approval_policy, - sandbox_policy, + permission_profile, .. } = event; // Header box rendered as history (so it appears at the very top) @@ -1251,7 +1252,7 @@ pub(crate) fn new_session_info( config.cwd.to_path_buf(), CODEX_CLI_VERSION, ) - .with_yolo_mode(has_yolo_permissions(approval_policy, &sandbox_policy)); + .with_yolo_mode(has_yolo_permissions(approval_policy, &permission_profile)); let mut parts: Vec> = vec![Box::new(header)]; if is_first_event { @@ -1313,14 +1314,23 @@ pub(crate) fn new_session_info( pub(crate) fn is_yolo_mode(config: &Config) -> bool { has_yolo_permissions( config.permissions.approval_policy.value(), - &config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), + &config.permissions.permission_profile(), ) } -fn has_yolo_permissions(approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy) -> bool { - approval_policy == AskForApproval::Never && *sandbox_policy == SandboxPolicy::DangerFullAccess +fn has_yolo_permissions( + approval_policy: AskForApproval, + permission_profile: &PermissionProfile, +) -> bool { + approval_policy == AskForApproval::Never + && matches!( + permission_profile, + PermissionProfile::Disabled + | PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled, + } + ) } pub(crate) fn new_user_prompt( @@ -2993,7 +3003,6 @@ mod tests { use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use dirs::home_dir; use pretty_assertions::assert_eq; @@ -3184,8 +3193,7 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/tmp/project").abs(), reasoning_effort: None, history_log_id: 0, @@ -4205,6 +4213,31 @@ mod tests { insta::assert_snapshot!(rendered); } + #[test] + fn yolo_mode_includes_managed_full_access_profiles() { + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled, + }; + + assert!(has_yolo_permissions( + AskForApproval::Never, + &permission_profile + )); + } + + #[test] + fn yolo_mode_excludes_external_sandbox_profiles() { + let permission_profile = PermissionProfile::External { + network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled, + }; + + assert!(!has_yolo_permissions( + AskForApproval::Never, + &permission_profile + )); + } + #[test] fn session_header_directory_center_truncates() { let mut dir = home_dir().expect("home directory");