From 4ddcab277b99d0e2f0546563e8b8e2a67befa822 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 21 Apr 2026 10:23:19 -0700 Subject: [PATCH] clients: send permission profiles to app-server --- codex-rs/exec/src/lib.rs | 46 ++++++++++-- codex-rs/exec/src/lib_tests.rs | 5 ++ codex-rs/tui/src/app/tests.rs | 24 +++++++ codex-rs/tui/src/app/thread_session_state.rs | 1 + codex-rs/tui/src/app_server_session.rs | 73 ++++++++++++++++++-- 5 files changed, 140 insertions(+), 9 deletions(-) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7352071a65..68f8567372 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -75,6 +75,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_otel::set_parent_from_context; use codex_otel::traceparent_context_from_env; use codex_protocol::config_types::SandboxMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; @@ -737,6 +738,18 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { items, output_schema, } => { + let (sandbox_policy, permission_profile) = if matches!( + default_sandbox_policy, + SandboxPolicy::ExternalSandbox { .. } + ) { + (Some(default_sandbox_policy.clone().into()), None) + } else { + let permission_profile = PermissionProfile::from_legacy_sandbox_policy( + default_sandbox_policy, + &default_cwd, + ); + (None, Some(permission_profile.into())) + }; let response: TurnStartResponse = send_request_with_response( &client, ClientRequest::TurnStart { @@ -748,8 +761,8 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { cwd: Some(default_cwd), approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, - sandbox_policy: Some(default_sandbox_policy.clone().into()), - permission_profile: None, + sandbox_policy, + permission_profile, model: None, service_tier: None, effort: default_effort, @@ -923,13 +936,19 @@ fn sandbox_mode_from_policy( } fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) + .flatten(); ThreadStartParams { model: config.model.clone(), model_provider: Some(config.model_provider_id.clone()), cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), ..ThreadStartParams::default() @@ -937,6 +956,11 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { } fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) + .flatten(); ThreadResumeParams { thread_id, model: config.model.clone(), @@ -944,12 +968,26 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ..ThreadResumeParams::default() } } +fn permission_profile_override_from_config( + config: &Config, +) -> Option { + if matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::ExternalSandbox { .. } + ) { + None + } else { + Some(config.permissions.permission_profile().into()) + } +} + fn config_request_overrides_from_config(config: &Config) -> Option> { config .active_profile diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 11f0be82ab..ba1ae9b102 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -361,6 +361,11 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_ params.approvals_reviewer, Some(codex_app_server_protocol::ApprovalsReviewer::User) ); + assert_eq!(params.sandbox, None); + assert_eq!( + params.permission_profile, + Some(config.permissions.permission_profile().into()) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 12d9904976..9feb429677 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2215,6 +2215,10 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2374,6 +2378,10 @@ async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2596,6 +2604,10 @@ async fn inactive_thread_approval_badge_clears_after_turn_completion_notificatio ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/agent"), + ), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2649,6 +2661,10 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/main"), + ), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2760,6 +2776,10 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + std::path::Path::new("/tmp/main"), + ), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -3675,6 +3695,10 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + cwd.as_path(), + ), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index a8fd34769e..17db50f876 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -24,6 +24,7 @@ impl App { approval_policy: self.config.permissions.approval_policy.value(), approvals_reviewer: self.config.approvals_reviewer, sandbox_policy: self.config.permissions.sandbox_policy.get().clone(), + permission_profile: self.config.permissions.permission_profile(), cwd: thread.cwd.clone(), instruction_source_paths: Vec::new(), reasoning_effort: self.chat_widget.current_reasoning_effort(), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 1dd8fc1bf1..80c1a7494a 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -83,6 +83,7 @@ use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelAvailabilityNux; use codex_protocol::openai_models::ModelPreset; @@ -146,6 +147,7 @@ pub(crate) struct ThreadSessionState { pub(crate) approval_policy: AskForApproval, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, pub(crate) sandbox_policy: SandboxPolicy, + pub(crate) permission_profile: PermissionProfile, pub(crate) cwd: AbsolutePathBuf, pub(crate) instruction_source_paths: Vec, pub(crate) reasoning_effort: Option, @@ -525,6 +527,14 @@ impl AppServerSession { output_schema: Option, ) -> Result { let request_id = self.next_request_id(); + let (sandbox_policy, permission_profile) = match sandbox_policy { + policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None), + policy => { + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd); + (None, Some(permission_profile.into())) + } + }; self.client .request_typed(ClientRequest::TurnStart { request_id, @@ -535,8 +545,8 @@ impl AppServerSession { cwd: Some(cwd), approval_policy: Some(approval_policy.into()), approvals_reviewer: Some(approvals_reviewer.into()), - sandbox_policy: Some(sandbox_policy.into()), - permission_profile: None, + sandbox_policy, + permission_profile, model: Some(model), service_tier, effort, @@ -1004,19 +1014,38 @@ fn sandbox_mode_from_policy( } } +fn permission_profile_override_from_config( + config: &Config, +) -> Option { + if matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::ExternalSandbox { .. } + ) { + None + } else { + Some(config.permissions.permission_profile().into()) + } +} + fn thread_start_params_from_config( config: &Config, thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, session_start_source: Option, ) -> ThreadStartParams { + let permission_profile = permission_profile_override_from_config(config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadStartParams { model: config.model.clone(), model_provider: thread_params_mode.model_provider_from_config(config), cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), session_start_source, @@ -1031,6 +1060,11 @@ fn thread_resume_params_from_config( thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, ) -> ThreadResumeParams { + let permission_profile = permission_profile_override_from_config(&config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadResumeParams { thread_id: thread_id.to_string(), model: config.model.clone(), @@ -1038,7 +1072,8 @@ fn thread_resume_params_from_config( cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(&config), persist_extended_history: true, ..ThreadResumeParams::default() @@ -1051,6 +1086,11 @@ fn thread_fork_params_from_config( thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, ) -> ThreadForkParams { + let permission_profile = permission_profile_override_from_config(&config); + let sandbox = permission_profile + .is_none() + .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .flatten(); ThreadForkParams { thread_id: thread_id.to_string(), model: config.model.clone(), @@ -1058,7 +1098,8 @@ fn thread_fork_params_from_config( cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), - sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()), + sandbox, + permission_profile, config: config_request_overrides_from_config(&config), base_instructions: config.base_instructions.clone(), developer_instructions: config.developer_instructions.clone(), @@ -1135,6 +1176,7 @@ async fn thread_session_state_from_thread_start_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1158,6 +1200,7 @@ async fn thread_session_state_from_thread_resume_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1181,6 +1224,7 @@ async fn thread_session_state_from_thread_fork_response( response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response.sandbox.to_core(), + response.permission_profile.clone().into(), response.cwd.clone(), response.instruction_sources.clone(), response.reasoning_effort, @@ -1223,6 +1267,7 @@ async fn thread_session_state_from_thread_response( approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, cwd: AbsolutePathBuf, instruction_source_paths: Vec, reasoning_effort: Option, @@ -1249,6 +1294,7 @@ async fn thread_session_state_from_thread_response( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, cwd, instruction_source_paths, reasoning_effort, @@ -1341,6 +1387,11 @@ mod tests { ); assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string())); + assert_eq!(params.sandbox, None); + assert_eq!( + params.permission_profile, + Some(config.permissions.permission_profile().into()) + ); assert_eq!(params.model_provider, Some(config.model_provider_id)); } @@ -1521,6 +1572,10 @@ mod tests { started.session.instruction_source_paths, response.instruction_sources ); + assert_eq!( + started.session.permission_profile, + response.permission_profile.clone().into() + ); assert_eq!(started.turns.len(), 1); assert_eq!(started.turns[0], response.thread.turns[0]); } @@ -1549,6 +1604,10 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), + PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, @@ -1579,6 +1638,10 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), + PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None,