From af95662a70bf665fd5629588a9f3a4f0f1ba7123 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:39:06 -0700 Subject: [PATCH 1/6] permissions: require profiles in TUI thread state (#19773) ## Why `ThreadSessionState` is the TUI's cached view of an app-server session. To make `PermissionProfile` the canonical runtime permissions model, cached thread sessions need to always have a profile instead of treating the profile as an optional supplement to a legacy `sandbox` response field. The main compatibility concern is older app-server v2 lifecycle responses that only include `sandbox` and omit `permissionProfile`: - `thread/start` -> `ThreadStartResponse.sandbox` - `thread/resume` -> `ThreadResumeResponse.sandbox` - `thread/fork` -> `ThreadForkResponse.sandbox` Those responses must still hydrate correctly when the TUI is pointed at an older app-server. This PR converts the legacy `sandbox` value into a `PermissionProfile` immediately at response ingestion time, using the response `cwd`, so cached sessions do not carry an optional profile that can later reinterpret cwd-bound grants against a different thread cwd. This fallback is intentionally boundary compatibility. The follow-up PRs in this stack continue the cleanup by making `SessionConfiguredEvent` profile-only, deriving sandbox projections from snapshots only when an API still needs them, and then removing `sandbox_policy` from `ThreadSessionState`. ## What Changed - Makes `ThreadSessionState.permission_profile` required. - Converts legacy app-server response `sandbox` values into a `PermissionProfile` at ingestion time using the response cwd. - Ensures `thread/read` hydration does not reuse a primary session profile that may be anchored to a different cwd; it uses the active widget permission settings for the read thread fallback instead of reusing cached primary-session permissions. - Keeps the app-server request path unchanged: embedded sessions send profiles, while remote sessions continue using legacy sandbox overrides for compatibility. ## Verification - `cargo test -p codex-tui thread_read --lib` - `cargo test -p codex-tui permission_settings_sync_preserves_active_profile_only_rules --lib` - `cargo test -p codex-tui resume_response_restores_turns_from_thread_items --lib` - `cargo test -p codex-tui thread_session_state::tests --lib` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19773). * #19900 * #19899 * #19776 * #19775 * #19774 * __->__ #19773 --- codex-rs/tui/src/app/tests.rs | 31 +++--- codex-rs/tui/src/app/thread_events.rs | 2 +- codex-rs/tui/src/app/thread_session_state.rs | 106 ++++++++++++++++--- codex-rs/tui/src/app_server_session.rs | 30 ++++-- codex-rs/tui/src/chatwidget.rs | 2 +- 5 files changed, 132 insertions(+), 39 deletions(-) diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index d9d3b0c026..8a08f8bf16 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2212,7 +2212,7 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2372,7 +2372,7 @@ 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: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2664,7 +2664,7 @@ 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: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2718,7 +2718,7 @@ 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: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2831,7 +2831,7 @@ 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: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2887,9 +2887,9 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ Ok(()) } -/// `thread/read` is metadata/replay hydration and does not return an -/// authoritative runtime `PermissionProfile`, so it must not reuse the active -/// primary session profile after swapping in the read thread's cwd. +/// `thread/read` is metadata/replay hydration and does not return a fresh +/// server-authored `PermissionProfile`, so it must not reuse the cached primary +/// session profile after swapping in the read thread's cwd. #[tokio::test] async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { let mut app = make_test_app().await; @@ -2900,7 +2900,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::workspace_write()), + permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; app.primary_session_configured = Some(primary_session); @@ -2931,10 +2931,15 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { assert_eq!(session.thread_id, read_thread_id); assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/read").as_path()); + let expected_permission_profile = app + .chat_widget + .config_ref() + .permissions + .permission_profile(); assert_eq!( - session.permission_profile, None, - "thread/read does not return an authoritative permission profile; reusing the primary \ - session profile would reinterpret cwd-bound entries against the read thread cwd" + session.permission_profile, expected_permission_profile, + "thread/read does not return fresh server permissions; the fallback profile must use the \ + active widget permissions rather than reusing the cached primary session profile" ); } @@ -3800,7 +3805,7 @@ 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: Some(PermissionProfile::read_only()), + permission_profile: PermissionProfile::read_only(), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 9bbb41fbd1..d73c39dfea 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -351,7 +351,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: Some(PermissionProfile::read_only()), + permission_profile: PermissionProfile::read_only(), 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 b4d0fb2684..a0ba5bdc30 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -3,6 +3,8 @@ use crate::app_server_session::ThreadSessionState; use crate::read_session_model; use codex_app_server_protocol::Thread; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::SandboxPolicy; impl App { pub(super) async fn sync_active_thread_permission_settings_to_cached_session(&mut self) { @@ -16,12 +18,11 @@ impl App { .config .permissions .legacy_sandbox_policy(self.config.cwd.as_path()); - let permission_profile = Some( - self.chat_widget - .config_ref() - .permissions - .permission_profile(), - ); + let permission_profile = self + .chat_widget + .config_ref() + .permissions + .permission_profile(); let update_session = |session: &mut ThreadSessionState| { session.approval_policy = approval_policy; session.approvals_reviewer = approvals_reviewer; @@ -48,10 +49,8 @@ impl App { thread_id: ThreadId, thread: &Thread, ) -> ThreadSessionState { - let sandbox_policy = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); + let sandbox_policy = self.active_legacy_sandbox_policy_for_cwd(thread.cwd.as_path()); + let permission_profile = self.active_permission_profile(); let mut session = self .primary_session_configured .clone() @@ -65,8 +64,8 @@ impl App { service_tier: self.chat_widget.current_service_tier(), approval_policy: self.config.permissions.approval_policy.value(), approvals_reviewer: self.config.approvals_reviewer, - sandbox_policy, - permission_profile: None, + sandbox_policy: sandbox_policy.clone(), + permission_profile: permission_profile.clone(), cwd: thread.cwd.clone(), instruction_source_paths: Vec::new(), reasoning_effort: self.chat_widget.current_reasoning_effort(), @@ -79,7 +78,8 @@ impl App { session.thread_name = thread.name.clone(); session.model_provider_id = thread.model_provider.clone(); session.cwd = thread.cwd.clone(); - session.permission_profile = None; + session.sandbox_policy = sandbox_policy; + session.permission_profile = permission_profile; session.instruction_source_paths = Vec::new(); session.rollout_path = thread.path.clone(); if let Some(model) = @@ -93,6 +93,20 @@ impl App { session.history_entry_count = 0; session } + + fn active_permission_profile(&self) -> PermissionProfile { + self.chat_widget + .config_ref() + .permissions + .permission_profile() + } + + fn active_legacy_sandbox_policy_for_cwd(&self, cwd: &std::path::Path) -> SandboxPolicy { + self.chat_widget + .config_ref() + .permissions + .legacy_sandbox_policy(cwd) + } } #[cfg(test)] @@ -128,7 +142,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: None, + permission_profile: PermissionProfile::read_only(), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, @@ -202,7 +216,7 @@ mod tests { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, sandbox_policy: expected_sandbox_policy, - permission_profile: Some(expected_permission_profile), + permission_profile: expected_permission_profile, ..main_session }; assert_eq!( @@ -256,7 +270,7 @@ mod tests { NetworkSandboxPolicy::Restricted, ); let session = ThreadSessionState { - permission_profile: Some(profile.clone()), + permission_profile: profile.clone(), ..test_thread_session(thread_id, test_path_buf("/tmp/main")) }; @@ -276,7 +290,7 @@ mod tests { let expected_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - permission_profile: Some(profile), + permission_profile: profile, ..session }; assert_eq!( @@ -295,4 +309,62 @@ mod tests { .clone(); assert_eq!(store_session, Some(expected_session)); } + + #[tokio::test] + async fn thread_read_fallback_uses_active_permission_settings() { + let mut app = make_test_app().await; + let primary_thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000404").expect("valid thread"); + let read_thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000405").expect("valid thread"); + let primary_session = ThreadSessionState { + permission_profile: PermissionProfile::workspace_write(), + ..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary")) + }; + let read_thread = Thread { + id: read_thread_id.to_string(), + forked_from_id: None, + preview: "read thread".to_string(), + ephemeral: false, + model_provider: "read-provider".to_string(), + created_at: 1, + updated_at: 2, + status: codex_app_server_protocol::ThreadStatus::Idle, + path: None, + cwd: test_path_buf("/tmp/read").abs(), + cli_version: "0.0.0".to_string(), + source: codex_app_server_protocol::SessionSource::Unknown, + agent_nickname: None, + agent_role: None, + git_info: None, + name: Some("read thread".to_string()), + turns: Vec::new(), + }; + + app.primary_session_configured = Some(primary_session.clone()); + app.chat_widget.handle_thread_session(primary_session); + + let session = app + .session_state_for_thread_read(read_thread_id, &read_thread) + .await; + + let expected_sandbox_policy = app + .chat_widget + .config_ref() + .permissions + .legacy_sandbox_policy(read_thread.cwd.as_path()); + let expected_permission_profile = app + .chat_widget + .config_ref() + .permissions + .permission_profile(); + assert_eq!(session.sandbox_policy, expected_sandbox_policy); + assert_eq!(session.permission_profile, expected_permission_profile); + assert_ne!( + session.permission_profile, + app.config.permissions.permission_profile(), + "thread/read fallback must use the active widget permissions rather than stale app \ + config defaults" + ); + } } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 11779c204c..5a33896dd3 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -156,13 +156,13 @@ pub(crate) struct ThreadSessionState { pub(crate) service_tier: Option, pub(crate) approval_policy: AskForApproval, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, - /// Legacy sandbox projection kept for compatibility. Use this only when - /// `permission_profile` is `None`. + /// Legacy sandbox projection kept only for compatibility fields that have + /// not migrated to `PermissionProfile` yet. pub(crate) sandbox_policy: SandboxPolicy, - /// Canonical active permissions when available. Consumers should prefer - /// this over `sandbox_policy`; `None` means the session only has a legacy - /// sandbox projection. - pub(crate) permission_profile: Option, + /// Canonical active permissions for this session. Legacy app-server + /// responses are converted to a profile at ingestion time using the + /// response cwd so cached sessions do not reinterpret cwd-bound grants. + pub(crate) permission_profile: PermissionProfile, pub(crate) cwd: AbsolutePathBuf, pub(crate) instruction_source_paths: Vec, pub(crate) reasoning_effort: Option, @@ -1407,6 +1407,8 @@ 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, @@ -1430,6 +1432,16 @@ 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 { @@ -1770,7 +1782,11 @@ mod tests { ); assert_eq!( started.session.permission_profile, - response.permission_profile.clone().map(Into::into) + response + .permission_profile + .clone() + .map(Into::into) + .expect("response includes profile") ); assert_eq!(started.turns.len(), 1); assert_eq!(started.turns[0], response.thread.turns[0]); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8fbb2fe832..f69eea4afc 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1621,7 +1621,7 @@ fn thread_session_state_to_legacy_event( approval_policy: session.approval_policy, approvals_reviewer: session.approvals_reviewer, sandbox_policy: session.sandbox_policy, - permission_profile: session.permission_profile, + permission_profile: Some(session.permission_profile), cwd: session.cwd, reasoning_effort: session.reasoning_effort, history_log_id: session.history_log_id, From 86b7c3fef29ff344d2c2ae85e4aca88630f831de Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:41:10 -0700 Subject: [PATCH 2/6] permissions: make SessionConfigured profile-only --- .../app-server/src/codex_message_processor.rs | 16 +-- codex-rs/core/src/session/session.rs | 4 +- codex-rs/core/src/session/tests.rs | 7 +- codex-rs/exec/src/lib.rs | 5 +- .../tests/event_processor_with_json_output.rs | 5 +- codex-rs/mcp-server/src/outgoing_message.rs | 19 +--- codex-rs/protocol/src/protocol.rs | 103 +++++++++++++++--- codex-rs/tui/src/app/config_persistence.rs | 4 +- codex-rs/tui/src/app/tests.rs | 18 +-- codex-rs/tui/src/chatwidget.rs | 35 +----- .../chatwidget/tests/composer_submission.rs | 29 ++--- .../tui/src/chatwidget/tests/exec_flow.rs | 3 +- .../src/chatwidget/tests/history_replay.rs | 30 ++--- .../tui/src/chatwidget/tests/permissions.rs | 23 ++-- .../tui/src/chatwidget/tests/plan_mode.rs | 6 +- .../src/chatwidget/tests/status_and_layout.rs | 3 +- codex-rs/tui/src/history_cell.rs | 43 ++++++-- 17 files changed, 192 insertions(+), 161 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6a1e6fbae8..c625e4c594 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -4235,9 +4235,9 @@ 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 permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile.clone()); let response = ThreadResumeResponse { thread, @@ -4248,7 +4248,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: config_snapshot.sandbox_policy.into(), permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -4831,9 +4831,9 @@ 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 permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile.clone()); let response = ThreadForkResponse { thread: thread.clone(), @@ -4844,7 +4844,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: config_snapshot.sandbox_policy.into(), permission_profile, reasoning_effort: session_configured.reasoning_effort, }; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index d4c0d337cf..176865ee6c 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 283220e8fa..c9416ac2b4 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 9e9d9bc148..fc3392db2f 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 3a7b5d0fcc..23dcd6733a 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 67ceb91dd8..78ef4cacff 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 85ef863b81..bd08e8ae47 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,67 @@ 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, + 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 +5141,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 +5153,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 +5173,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 +5185,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 9f6b631226..3164655374 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 8a08f8bf16..6c82dac638 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 f69eea4afc..2b18718860 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 302c4ea38c..d21febaa81 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 4d87914aa4..ec1d03f9fc 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 77f52524f4..443cac0532 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 388bc67f81..5018e6fea8 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,16 @@ 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::from_legacy_sandbox_policy_for_cwd( + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![extra_root], + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + cwd.as_path(), + ); chat.handle_codex_event(Event { id: "session-configured-custom-workspace".to_string(), @@ -531,14 +540,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 65f6ae0c31..c6e1f73763 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 d7aefbe4b4..19890a5d3c 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 33e2f25c63..af19f064f4 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,19 @@ 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 session_header_directory_center_truncates() { let mut dir = home_dir().expect("home directory"); From 7497c0207a12d167f44cf34734bd334bdbfcdb56 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:41:10 -0700 Subject: [PATCH 3/6] permissions: derive snapshot sandbox projections --- .../app-server/src/codex_message_processor.rs | 18 +++++++++--------- codex-rs/core/src/codex_thread.rs | 13 ++++++++++++- codex-rs/core/src/memories/tests.rs | 8 ++++---- codex-rs/core/src/session/session.rs | 1 - .../src/tools/handlers/multi_agents_tests.rs | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index c625e4c594..49eef9a813 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2712,6 +2712,7 @@ impl CodexMessageProcessor { /*has_in_progress_turn*/ false, ); + let sandbox = config_snapshot.sandbox_policy().into(); let permission_profile = thread_response_permission_profile(config_snapshot.permission_profile); @@ -2724,7 +2725,7 @@ impl CodexMessageProcessor { instruction_sources, approval_policy: config_snapshot.approval_policy.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(), - sandbox: config_snapshot.sandbox_policy.into(), + sandbox, permission_profile, reasoning_effort: config_snapshot.reasoning_effort, }; @@ -3284,7 +3285,7 @@ impl CodexMessageProcessor { builder.model_provider = Some(model_provider.clone()); builder.cwd = config_snapshot.cwd.to_path_buf(); builder.cli_version = Some(env!("CARGO_PKG_VERSION").to_string()); - builder.sandbox_policy = config_snapshot.sandbox_policy.clone(); + builder.sandbox_policy = config_snapshot.sandbox_policy(); builder.approval_mode = config_snapshot.approval_policy; let metadata = builder.build(model_provider.as_str()); if let Err(err) = state_db_ctx.insert_thread_if_absent(&metadata).await { @@ -4248,7 +4249,7 @@ impl CodexMessageProcessor { instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), - sandbox: config_snapshot.sandbox_policy.into(), + sandbox: config_snapshot.sandbox_policy().into(), permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -4844,7 +4845,7 @@ impl CodexMessageProcessor { instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), - sandbox: config_snapshot.sandbox_policy.into(), + sandbox: config_snapshot.sandbox_policy().into(), permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -8085,13 +8086,13 @@ async fn handle_pending_thread_resume_request( tracing::warn!("failed to apply goal resume runtime effects: {err}"); } + let sandbox_policy = pending.config_snapshot.sandbox_policy(); let ThreadConfigSnapshot { model, model_provider_id, service_tier, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, cwd, reasoning_effort, @@ -8314,8 +8315,9 @@ fn collect_resume_override_mismatches( } } if let Some(requested_sandbox) = request.sandbox.as_ref() { + let active_sandbox = config_snapshot.sandbox_policy(); let sandbox_matches = matches!( - (requested_sandbox, &config_snapshot.sandbox_policy), + (requested_sandbox, &active_sandbox), ( SandboxMode::ReadOnly, codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } @@ -8332,8 +8334,7 @@ fn collect_resume_override_mismatches( ); if !sandbox_matches { mismatch_details.push(format!( - "sandbox requested={requested_sandbox:?} active={:?}", - config_snapshot.sandbox_policy + "sandbox requested={requested_sandbox:?} active={active_sandbox:?}" )); } } @@ -10019,7 +10020,6 @@ mod tests { service_tier: Some(codex_protocol::config_types::ServiceTier::Flex), approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, permission_profile: codex_protocol::models::PermissionProfile::Disabled, cwd, ephemeral: false, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 7454b98651..fa4bd8067c 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -47,7 +47,6 @@ pub struct ThreadConfigSnapshot { pub service_tier: Option, pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, - pub sandbox_policy: SandboxPolicy, pub permission_profile: PermissionProfile, pub cwd: AbsolutePathBuf, pub ephemeral: bool, @@ -56,6 +55,18 @@ pub struct ThreadConfigSnapshot { pub session_source: SessionSource, } +impl ThreadConfigSnapshot { + pub fn sandbox_policy(&self) -> SandboxPolicy { + let file_system_sandbox_policy = self.permission_profile.file_system_sandbox_policy(); + codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( + &self.permission_profile, + &file_system_sandbox_policy, + self.permission_profile.network_sandbox_policy(), + self.cwd.as_path(), + ) + } +} + /// Turn context overrides that app-server validates before starting a turn. #[derive(Clone, Default)] pub struct CodexThreadTurnContextOverrides { diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index c00c77b8e4..cbf7222b1a 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -883,11 +883,11 @@ mod phase2 { config_snapshot.cwd.as_path(), memory_root(&harness.config.codex_home).as_path() ); - match &config_snapshot.sandbox_policy { + let sandbox_policy = config_snapshot.sandbox_policy(); + match &sandbox_policy { SandboxPolicy::WorkspaceWrite { network_access, .. } => { assert!(!*network_access); - let effective_writable_roots: Vec<_> = config_snapshot - .sandbox_policy + let effective_writable_roots: Vec<_> = sandbox_policy .get_writable_roots_with_cwd(config_snapshot.cwd.as_path()) .into_iter() .map(|root| root.root) @@ -917,7 +917,7 @@ mod phase2 { let file_system_sandbox_policy = turn_context.file_system_sandbox_policy(); let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &config_snapshot.sandbox_policy, + &sandbox_policy, config_snapshot.cwd.as_path(), ); assert!( diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 176865ee6c..1d37db1b8d 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -126,7 +126,6 @@ impl SessionConfiguration { service_tier: self.service_tier, approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, - sandbox_policy: self.sandbox_policy(), permission_profile: self.permission_profile(), cwd: self.cwd.clone(), ephemeral: self.original_config_do_not_use.ephemeral, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 64cc9db032..fc861be61c 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2143,7 +2143,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { .expect("spawned agent thread should exist") .config_snapshot() .await; - assert_eq!(snapshot.sandbox_policy, expected_sandbox); + assert_eq!(snapshot.sandbox_policy(), expected_sandbox); assert_eq!(snapshot.approval_policy, AskForApproval::OnRequest); assert_eq!(snapshot.permission_profile, expected_permission_profile); let child_thread = manager From 7edd6bde2e4878685bc1f00beffc163294a1d3ae Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:41:10 -0700 Subject: [PATCH 4/6] permissions: store thread sessions as profiles --- codex-rs/tui/src/app/tests.rs | 9 +------ codex-rs/tui/src/app/thread_events.rs | 2 -- codex-rs/tui/src/app/thread_session_state.rs | 26 +------------------- codex-rs/tui/src/app_server_session.rs | 4 --- 4 files changed, 2 insertions(+), 39 deletions(-) diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 6c82dac638..9477030bc5 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2211,7 +2211,6 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { /*capacity*/ 1, ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2371,7 +2370,6 @@ async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> /*capacity*/ 4, ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2663,7 +2661,6 @@ async fn inactive_thread_approval_badge_clears_after_turn_completion_notificatio /*capacity*/ 4, ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2717,7 +2714,6 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re ThreadId::from_string("00000000-0000-0000-0000-000000000202").expect("valid thread"); let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2742,7 +2738,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re current_date: None, timezone: None, approval_policy: primary_session.approval_policy, - sandbox_policy: primary_session.sandbox_policy.clone(), + sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -2830,7 +2826,6 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ ThreadId::from_string("00000000-0000-0000-0000-000000000302").expect("valid thread"); let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2899,7 +2894,6 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { ThreadId::from_string("00000000-0000-0000-0000-000000000402").expect("valid thread"); let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -3803,7 +3797,6 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: PermissionProfile::read_only(), cwd: cwd.abs(), instruction_source_paths: Vec::new(), diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index d73c39dfea..10fc370f58 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -335,7 +335,6 @@ mod tests { use codex_config::types::ApprovalsReviewer; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -350,7 +349,6 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: PermissionProfile::read_only(), cwd: cwd.abs(), instruction_source_paths: Vec::new(), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index a0ba5bdc30..0ff1cec26b 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -4,7 +4,6 @@ use crate::read_session_model; use codex_app_server_protocol::Thread; use codex_protocol::ThreadId; use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::SandboxPolicy; impl App { pub(super) async fn sync_active_thread_permission_settings_to_cached_session(&mut self) { @@ -14,10 +13,6 @@ impl App { let approval_policy = self.config.permissions.approval_policy.value(); let approvals_reviewer = self.config.approvals_reviewer; - let sandbox_policy = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); let permission_profile = self .chat_widget .config_ref() @@ -26,7 +21,6 @@ impl App { let update_session = |session: &mut ThreadSessionState| { session.approval_policy = approval_policy; session.approvals_reviewer = approvals_reviewer; - session.sandbox_policy = sandbox_policy.clone(); session.permission_profile = permission_profile.clone(); }; @@ -49,7 +43,6 @@ impl App { thread_id: ThreadId, thread: &Thread, ) -> ThreadSessionState { - let sandbox_policy = self.active_legacy_sandbox_policy_for_cwd(thread.cwd.as_path()); let permission_profile = self.active_permission_profile(); let mut session = self .primary_session_configured @@ -64,7 +57,6 @@ impl App { service_tier: self.chat_widget.current_service_tier(), approval_policy: self.config.permissions.approval_policy.value(), approvals_reviewer: self.config.approvals_reviewer, - sandbox_policy: sandbox_policy.clone(), permission_profile: permission_profile.clone(), cwd: thread.cwd.clone(), instruction_source_paths: Vec::new(), @@ -78,7 +70,6 @@ impl App { session.thread_name = thread.name.clone(); session.model_provider_id = thread.model_provider.clone(); session.cwd = thread.cwd.clone(); - session.sandbox_policy = sandbox_policy; session.permission_profile = permission_profile; session.instruction_source_paths = Vec::new(); session.rollout_path = thread.path.clone(); @@ -100,13 +91,6 @@ impl App { .permissions .permission_profile() } - - fn active_legacy_sandbox_policy_for_cwd(&self, cwd: &std::path::Path) -> SandboxPolicy { - self.chat_widget - .config_ref() - .permissions - .legacy_sandbox_policy(cwd) - } } #[cfg(test)] @@ -141,7 +125,6 @@ mod tests { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: PermissionProfile::read_only(), cwd: cwd.abs(), instruction_source_paths: Vec::new(), @@ -163,7 +146,7 @@ mod tests { let main_session = test_thread_session(main_thread_id, test_path_buf("/tmp/main")); let side_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + permission_profile: PermissionProfile::workspace_write(), ..test_thread_session(side_thread_id, test_path_buf("/tmp/side")) }; @@ -215,7 +198,6 @@ mod tests { let expected_main_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, - sandbox_policy: expected_sandbox_policy, permission_profile: expected_permission_profile, ..main_session }; @@ -348,17 +330,11 @@ mod tests { .session_state_for_thread_read(read_thread_id, &read_thread) .await; - let expected_sandbox_policy = app - .chat_widget - .config_ref() - .permissions - .legacy_sandbox_policy(read_thread.cwd.as_path()); let expected_permission_profile = app .chat_widget .config_ref() .permissions .permission_profile(); - assert_eq!(session.sandbox_policy, expected_sandbox_policy); assert_eq!(session.permission_profile, expected_permission_profile); assert_ne!( session.permission_profile, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 5a33896dd3..0d38449db9 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -156,9 +156,6 @@ pub(crate) struct ThreadSessionState { pub(crate) service_tier: Option, pub(crate) approval_policy: AskForApproval, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, - /// Legacy sandbox projection kept only for compatibility fields that have - /// not migrated to `PermissionProfile` yet. - pub(crate) sandbox_policy: SandboxPolicy, /// Canonical active permissions for this session. Legacy app-server /// responses are converted to a profile at ingestion time using the /// response cwd so cached sessions do not reinterpret cwd-bound grants. @@ -1420,7 +1417,6 @@ async fn thread_session_state_from_thread_response( service_tier, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, cwd, instruction_source_paths, From 737f95d313c708b0ae988590e91ca0b80efaa5f6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:41:10 -0700 Subject: [PATCH 5/6] app-server-protocol: mark permission profiles experimental --- .../schema/json/ClientRequest.json | 55 -------- .../codex_app_server_protocol.schemas.json | 91 ------------- .../codex_app_server_protocol.v2.schemas.json | 91 ------------- .../schema/json/v2/CommandExecParams.json | 11 -- .../schema/json/v2/ThreadForkParams.json | 11 -- .../schema/json/v2/ThreadForkResponse.json | 12 -- .../schema/json/v2/ThreadResumeParams.json | 11 -- .../schema/json/v2/ThreadResumeResponse.json | 12 -- .../schema/json/v2/ThreadStartParams.json | 11 -- .../schema/json/v2/ThreadStartResponse.json | 12 -- .../schema/json/v2/TurnStartParams.json | 11 -- .../schema/typescript/v2/CommandExecParams.ts | 49 ++----- .../CommandExecutionRequestApprovalParams.ts | 36 ++---- .../schema/typescript/v2/ThreadForkParams.ts | 17 +-- .../typescript/v2/ThreadForkResponse.ts | 16 +-- .../typescript/v2/ThreadResumeParams.ts | 23 +--- .../typescript/v2/ThreadResumeResponse.ts | 16 +-- .../schema/typescript/v2/ThreadStartParams.ts | 15 +-- .../typescript/v2/ThreadStartResponse.ts | 16 +-- .../schema/typescript/v2/TurnStartParams.ts | 15 +-- codex-rs/app-server-protocol/src/export.rs | 120 +++++++++++++++++- .../src/protocol/common.rs | 28 ++++ .../app-server-protocol/src/protocol/v2.rs | 10 +- 23 files changed, 193 insertions(+), 496 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 079a8a2d76..31cfd542b7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -218,17 +218,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -3271,17 +3260,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -3687,17 +3665,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -3881,17 +3848,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -4103,17 +4059,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 7e34967470..1c0ace7fd8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6536,17 +6536,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -14443,17 +14432,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -14522,18 +14500,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -15967,17 +15933,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -16056,18 +16011,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -16284,17 +16227,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -16383,18 +16315,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -17100,17 +17020,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 406795ff54..cf721d82af 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -3055,17 +3055,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -12329,17 +12318,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -12408,18 +12386,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -13853,17 +13819,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -13942,18 +13897,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -14170,17 +14113,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -14269,18 +14201,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -14986,17 +14906,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index 7db4d635ee..f29483862c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -505,17 +505,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 504dbdc67e..eb8f3bdb4f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -473,17 +473,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 3ab0077d0a..b5d6b139b2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 63d0345dae..425fc52a48 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1328,17 +1328,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 12c38db5dc..7135a53174 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index bc8d83d2bc..d5e76f05e3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -526,17 +526,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 6e1637ec80..5deca9f699 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index d866c03152..719a1bedc8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -829,17 +829,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts index 659974feaf..221a2399c1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts @@ -2,7 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; /** @@ -13,12 +12,10 @@ import type { SandboxPolicy } from "./SandboxPolicy"; * sent only after all `command/exec/outputDelta` notifications for that * connection have been emitted. */ -export type CommandExecParams = { -/** +export type CommandExecParams = {/** * Command argv vector. Empty arrays are rejected. */ -command: Array, -/** +command: Array, /** * Optional client-supplied, connection-scoped process id. * * Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up @@ -26,81 +23,63 @@ command: Array, * `command/exec/terminate` calls. When omitted, buffered execution gets an * internal id that is not exposed to the client. */ -processId?: string | null, -/** +processId?: string | null, /** * Enable PTY mode. * * This implies `streamStdin` and `streamStdoutStderr`. */ -tty?: boolean, -/** +tty?: boolean, /** * Allow follow-up `command/exec/write` requests to write stdin bytes. * * Requires a client-supplied `processId`. */ -streamStdin?: boolean, -/** +streamStdin?: boolean, /** * Stream stdout/stderr via `command/exec/outputDelta` notifications. * * Streamed bytes are not duplicated into the final response and require a * client-supplied `processId`. */ -streamStdoutStderr?: boolean, -/** +streamStdoutStderr?: boolean, /** * Optional per-stream stdout/stderr capture cap in bytes. * * When omitted, the server default applies. Cannot be combined with * `disableOutputCap`. */ -outputBytesCap?: number | null, -/** +outputBytesCap?: number | null, /** * Disable stdout/stderr capture truncation for this request. * * Cannot be combined with `outputBytesCap`. */ -disableOutputCap?: boolean, -/** +disableOutputCap?: boolean, /** * Disable the timeout entirely for this request. * * Cannot be combined with `timeoutMs`. */ -disableTimeout?: boolean, -/** +disableTimeout?: boolean, /** * Optional timeout in milliseconds. * * When omitted, the server default applies. Cannot be combined with * `disableTimeout`. */ -timeoutMs?: number | null, -/** +timeoutMs?: number | null, /** * Optional working directory. Defaults to the server cwd. */ -cwd?: string | null, -/** +cwd?: string | null, /** * Optional environment overrides merged into the server-computed * environment. * * Matching names override inherited values. Set a key to `null` to unset * an inherited variable. */ -env?: { [key in string]?: string | null } | null, -/** +env?: { [key in string]?: string | null } | null, /** * Optional initial PTY size in character cells. Only valid when `tty` is * true. */ -size?: CommandExecTerminalSize | null, -/** +size?: CommandExecTerminalSize | null, /** * Optional sandbox policy for this command. * * Uses the same shape as thread/turn execution sandbox configuration and * defaults to the user's configured policy when omitted. Cannot be * combined with `permissionProfile`. */ -sandboxPolicy?: SandboxPolicy | null, -/** - * Optional full permissions profile for this command. - * - * Defaults to the user's configured permissions when omitted. Cannot be - * combined with `sandboxPolicy`. - */ -permissionProfile?: PermissionProfile | null, }; +sandboxPolicy?: SandboxPolicy | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index 59da1de945..ca2d0b0aa0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -2,15 +2,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string, -/** +export type CommandExecutionRequestApprovalParams = {threadId: string, turnId: string, itemId: string, /** * Unique identifier for this specific approval callback. * * For regular shell/unified_exec approvals, this is null. @@ -19,40 +16,25 @@ export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: * one parent `itemId`, so `approvalId` is a distinct opaque callback id * (a UUID) used to disambiguate routing. */ -approvalId?: string | null, -/** +approvalId?: string | null, /** * Optional explanatory reason (e.g. request for network access). */ -reason?: string | null, -/** +reason?: string | null, /** * Optional context for a managed-network approval prompt. */ -networkApprovalContext?: NetworkApprovalContext | null, -/** +networkApprovalContext?: NetworkApprovalContext | null, /** * The command to be executed. */ -command?: string | null, -/** +command?: string | null, /** * The command's working directory. */ -cwd?: AbsolutePathBuf | null, -/** +cwd?: AbsolutePathBuf | null, /** * Best-effort parsed command actions for friendly display. */ -commandActions?: Array | null, -/** - * Optional additional permissions requested for this command. - */ -additionalPermissions?: AdditionalPermissionProfile | null, -/** +commandActions?: Array | null, /** * Optional proposed execpolicy amendment to allow similar commands without prompting. */ -proposedExecpolicyAmendment?: ExecPolicyAmendment | null, -/** +proposedExecpolicyAmendment?: ExecPolicyAmendment | null, /** * Optional proposed network policy amendments (allow/deny host) for future requests. */ -proposedNetworkPolicyAmendments?: Array | null, -/** - * Ordered list of decisions the client may present for this prompt. - */ -availableDecisions?: Array | null, }; +proposedNetworkPolicyAmendments?: Array | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index f5f3f1878c..a40e406df9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -5,7 +5,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -18,27 +17,15 @@ import type { SandboxMode } from "./SandboxMode"; * Prefer using thread_id whenever possible. */ export type ThreadForkParams = {threadId: string, /** - * [UNSTABLE] Specify the rollout path to fork from. - * If specified, the thread_id param will be ignored. - */ -path?: string | null, /** * Configuration overrides for the forked thread, if any. */ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for the forked thread. Cannot be combined - * with `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * When true, return only thread metadata and live fork state without * populating `thread.turns`. This is useful when the client plans to call * `thread/turns/list` immediately after forking. */ -excludeTurns?: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on subsequent resume/fork/read. - */ -persistExtendedHistory: boolean}; +excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index b69f1da012..1207002514 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 452126be46..f9821585fb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -2,12 +2,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; -import type { ResponseItem } from "../ResponseItem"; import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -22,32 +20,15 @@ import type { SandboxMode } from "./SandboxMode"; * Prefer using thread_id whenever possible. */ export type ThreadResumeParams = {threadId: string, /** - * [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. - * If specified, the thread will be resumed with the provided history - * instead of loaded from disk. - */ -history?: Array | null, /** - * [UNSTABLE] Specify the rollout path to resume from. - * If specified, the thread_id param will be ignored. - */ -path?: string | null, /** * Configuration overrides for the resumed thread, if any. */ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for the resumed thread. Cannot be combined - * with `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * When true, return only thread metadata and live-resume state without * populating `thread.turns`. This is useful when the client plans to call * `thread/turns/list` immediately after resuming. */ -excludeTurns?: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on subsequent resume/fork/read. - */ -persistExtendedHistory: boolean}; +excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index 5ceec7f3fe..20f91b3e91 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 8b9dafec9f..374ac2e681 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,7 +6,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; import type { ThreadStartSource } from "./ThreadStartSource"; @@ -14,16 +13,4 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for this thread. Cannot be combined with - * `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** - * If true, opt into emitting raw Responses API items on the event stream. - * This is for internal use only (e.g. Codex Cloud). - */ -experimentalRawEvents: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on resume/fork/read. - */ -persistExtendedHistory: boolean}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 61d268afe8..87e15411a2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 3d12e6001c..4af17115c8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -1,7 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationMode } from "../CollaborationMode"; import type { Personality } from "../Personality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; @@ -9,7 +8,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; @@ -27,10 +25,6 @@ approvalsReviewer?: ApprovalsReviewer | null, /** * Override the sandbox policy for this turn and subsequent turns. */ sandboxPolicy?: SandboxPolicy | null, /** - * Override the full permissions profile for this turn and subsequent - * turns. Cannot be combined with `sandboxPolicy`. - */ -permissionProfile?: PermissionProfile | null, /** * Override the model for this turn and subsequent turns. */ model?: string | null, /** @@ -49,11 +43,4 @@ personality?: Personality | null, /** * Optional JSON Schema used to constrain the final assistant message for * this turn. */ -outputSchema?: JsonValue | null, /** - * EXPERIMENTAL - Set a pre-set collaboration mode. - * Takes precedence over model, reasoning_effort, and developer instructions if set. - * - * For `collaboration_mode.settings.developer_instructions`, `null` means - * "use the built-in instructions for the selected mode". - */ -collaborationMode?: CollaborationMode | null}; +outputSchema?: JsonValue | null}; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 96bb8d17a9..0f9b33671b 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -736,11 +736,11 @@ fn find_top_level_brace_span(input: &str) -> Option<(usize, usize)> { let mut state = ScanState::default(); let mut open_index = None; for (index, ch) in input.char_indices() { - if !state.in_string() && ch == '{' && state.depth.is_top_level() { + if !state.in_ignored_syntax() && ch == '{' && state.depth.is_top_level() { open_index = Some(index); } state.observe(ch); - if !state.in_string() + if !state.in_ignored_syntax() && ch == '}' && state.depth.is_top_level() && let Some(open) = open_index @@ -760,7 +760,7 @@ fn split_top_level_multi(input: &str, delimiters: &[char]) -> Vec { let mut start = 0usize; let mut parts = Vec::new(); for (index, ch) in input.char_indices() { - if !state.in_string() && state.depth.is_top_level() && delimiters.contains(&ch) { + if !state.in_ignored_syntax() && state.depth.is_top_level() && delimiters.contains(&ch) { let part = input[start..index].trim(); if !part.is_empty() { parts.push(part.to_string()); @@ -882,22 +882,58 @@ struct ScanState { depth: Depth, string_delim: Option, escape: bool, + block_comment: bool, + line_comment: bool, + previous_char: Option, } impl ScanState { fn observe(&mut self, ch: char) { + if self.line_comment { + if ch == '\n' { + self.line_comment = false; + } + self.previous_char = Some(ch); + return; + } + + if self.block_comment { + if self.previous_char == Some('*') && ch == '/' { + self.block_comment = false; + self.previous_char = None; + } else { + self.previous_char = Some(ch); + } + return; + } + if let Some(delim) = self.string_delim { if self.escape { self.escape = false; + self.previous_char = Some(ch); return; } if ch == '\\' { self.escape = true; + self.previous_char = Some(ch); return; } if ch == delim { self.string_delim = None; } + self.previous_char = Some(ch); + return; + } + + if self.previous_char == Some('/') && ch == '/' { + self.line_comment = true; + self.previous_char = Some(ch); + return; + } + + if self.previous_char == Some('/') && ch == '*' { + self.block_comment = true; + self.previous_char = Some(ch); return; } @@ -919,10 +955,11 @@ impl ScanState { } _ => {} } + self.previous_char = Some(ch); } - fn in_string(&self) -> bool { - self.string_delim.is_some() + fn in_ignored_syntax(&self) -> bool { + self.string_delim.is_some() || self.block_comment || self.line_comment } } @@ -2694,6 +2731,79 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k Ok(()) } + #[test] + fn experimental_type_fields_ts_filter_handles_generated_command_params_shape() -> Result<()> { + let output_dir = std::env::temp_dir().join(format!("codex_ts_filter_{}", Uuid::now_v7())); + fs::create_dir_all(&output_dir)?; + + struct TempDirGuard(PathBuf); + + impl Drop for TempDirGuard { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + let _guard = TempDirGuard(output_dir.clone()); + let path = output_dir.join("CommandExecParams.ts"); + let content = r#"import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; +import type { PermissionProfile } from "./PermissionProfile"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type CommandExecParams = {/** + * Command argv vector. Empty arrays are rejected. + */ +command: Array, /** + * Optional environment overrides merged into the server-computed + * environment. + */ +env?: { [key in string]?: string | null } | null, /** + * Optional initial PTY size in character cells. Only valid when `tty` is + * true. + */ +size?: CommandExecTerminalSize | null, /** + * Optional sandbox policy for this command. + * + * Uses the same shape as thread/turn execution sandbox configuration and + * defaults to the user's configured policy when omitted. Cannot be + * combined with `permissionProfile`. + */ +sandboxPolicy?: SandboxPolicy | null, +/** + * Optional full permissions profile for this command. + * + * Defaults to the user's configured permissions when omitted. Cannot be + * combined with `sandboxPolicy`. + */ +permissionProfile?: PermissionProfile | null}; +"#; + fs::write(&path, content)?; + + static CUSTOM_FIELD: crate::experimental_api::ExperimentalField = + crate::experimental_api::ExperimentalField { + type_name: "CommandExecParams", + field_name: "permissionProfile", + reason: "command/exec.permissionProfile", + }; + filter_experimental_type_fields_ts(&output_dir, &[&CUSTOM_FIELD])?; + + let filtered = fs::read_to_string(&path)?; + assert_eq!( + filtered.contains("permissionProfile?: PermissionProfile"), + false + ); + assert_eq!( + filtered.contains(r#"import type { PermissionProfile } from "./PermissionProfile";"#), + false + ); + assert_eq!(filtered.contains("sandboxPolicy?: SandboxPolicy"), true); + assert_eq!( + filtered.contains(r#"import type { SandboxPolicy } from "./SandboxPolicy";"#), + true + ); + Ok(()) + } + #[test] fn stable_schema_filter_removes_mock_experimental_method() -> Result<()> { let output_dir = std::env::temp_dir().join(format!("codex_schema_{}", Uuid::now_v7())); diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 016d6e16b8..167435ac88 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -581,6 +581,7 @@ client_request_definitions! { /// Execute a standalone command (argv vector) under the server's sandbox. OneOffCommandExec => "command/exec" { params: v2::CommandExecParams, + inspect_params: true, response: v2::CommandExecResponse, }, /// Write stdin bytes to a running `command/exec` session or close stdin. @@ -2049,6 +2050,33 @@ mod tests { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); assert_eq!(reason, Some("mock/experimentalMethod")); } + + #[test] + fn command_exec_permission_profile_is_marked_experimental() { + let request = ClientRequest::OneOffCommandExec { + request_id: RequestId::Integer(1), + params: v2::CommandExecParams { + command: vec!["pwd".to_string()], + process_id: None, + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: None, + permission_profile: Some(v2::PermissionProfile::Disabled), + }, + }; + + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("command/exec.permissionProfile")); + } + #[test] fn thread_realtime_start_is_marked_experimental() { let request = ClientRequest::ThreadRealtimeStart { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e1c941019d..bfb1eaac07 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3163,7 +3163,7 @@ pub struct CommandExecTerminalSize { /// The final `command/exec` response is deferred until the process exits and is /// sent only after all `command/exec/outputDelta` notifications for that /// connection have been emitted. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct CommandExecParams { @@ -3242,6 +3242,7 @@ pub struct CommandExecParams { /// /// Defaults to the user's configured permissions when omitted. Cannot be /// combined with `sandboxPolicy`. + #[experimental("command/exec.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, } @@ -3364,6 +3365,7 @@ pub struct ThreadStartParams { pub sandbox: Option, /// Full permissions override for this thread. Cannot be combined with /// `sandbox`. + #[experimental("thread/start.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3447,6 +3449,7 @@ pub struct ThreadStartResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/start.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3508,6 +3511,7 @@ pub struct ThreadResumeParams { pub sandbox: Option, /// Full permissions override for the resumed thread. Cannot be combined /// with `sandbox`. + #[experimental("thread/resume.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3551,6 +3555,7 @@ pub struct ThreadResumeResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/resume.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3603,6 +3608,7 @@ pub struct ThreadForkParams { pub sandbox: Option, /// Full permissions override for the forked thread. Cannot be combined /// with `sandbox`. + #[experimental("thread/fork.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3646,6 +3652,7 @@ pub struct ThreadForkResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/fork.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -5184,6 +5191,7 @@ pub struct TurnStartParams { pub sandbox_policy: Option, /// Override the full permissions profile for this turn and subsequent /// turns. Cannot be combined with `sandboxPolicy`. + #[experimental("turn/start.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, /// Override the model for this turn and subsequent turns. From b7dc64bf3d7a33252aab9813fb27aae094a01f42 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 20:41:10 -0700 Subject: [PATCH 6/6] permissions: add built-in default profiles --- codex-rs/config/src/config_toml.rs | 3 +- codex-rs/core/config.schema.json | 2 +- codex-rs/core/src/config/config_tests.rs | 187 +++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 73 ++++----- codex-rs/core/src/config/permissions.rs | 112 ++++++++++++++ 5 files changed, 335 insertions(+), 42 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index a1692b2cb1..3bd19f5568 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -114,7 +114,8 @@ pub struct ConfigToml { /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. pub sandbox_workspace_write: Option, - /// Default named permissions profile to apply from the `[permissions]` + /// Default permissions profile to apply. Names starting with `:` refer to + /// built-in profiles; other names are resolved from the `[permissions]` /// table. pub default_permissions: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index bdccafcb53..0758d14ba9 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2494,7 +2494,7 @@ "type": "string" }, "default_permissions": { - "description": "Default named permissions profile to apply from the `[permissions]` table.", + "description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.", "type": "string" }, "developer_instructions": { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 21a86dcadd..65a70804c5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1189,6 +1189,193 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<( Ok(()) } +#[tokio::test] +async fn default_permissions_can_select_builtin_profile_without_permissions_table() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":workspace".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert!( + policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected :workspace to allow writing the project root, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path()), + "expected :workspace to protect project metadata, policy: {policy:?}" + ); + Ok(()) +} + +#[tokio::test] +async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let project_key = cwd.path().to_string_lossy().to_string(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + projects: Some(HashMap::from([( + project_key, + ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }, + )])), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + if cfg!(target_os = "windows") { + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}" + ); + } else { + assert!( + policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected trusted project fallback to use :workspace, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()), + "expected :workspace metadata carveouts, policy: {policy:?}" + ); + } + Ok(()) +} + +#[tokio::test] +async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() -> std::io::Result<()> +{ + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert!( + policy.can_read_path_with_cwd(cwd.path(), cwd.path()), + "expected :read-only to allow reads, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected :read-only to deny writes, policy: {policy:?}" + ); + Ok(()) +} + +#[tokio::test] +async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":danger-no-sandbox".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!( + config.permissions.permission_profile(), + PermissionProfile::Disabled + ); + Ok(()) +} + +#[tokio::test] +async fn user_defined_permission_profile_names_cannot_use_builtin_prefix() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let err = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":custom".to_string()), + permissions: Some(PermissionsToml { + entries: BTreeMap::from([( + ":custom".to_string(), + PermissionProfileToml::default(), + )]), + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await + .expect_err("reserved profile name should be rejected"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "permissions profile `:custom` uses a reserved built-in profile prefix" + ); + Ok(()) +} + +#[tokio::test] +async fn unknown_builtin_permission_profile_name_is_rejected() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let err = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":unknown".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await + .expect_err("unknown built-in profile name should be rejected"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "default_permissions refers to unknown built-in profile `:unknown`" + ); + Ok(()) +} + #[tokio::test] async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f4a73dd25f..9fc0141003 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -99,9 +99,12 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -use crate::config::permissions::compile_permission_profile; +use crate::config::permissions::builtin_permission_profile; +use crate::config::permissions::compile_permission_profile_selection; +use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; -use crate::config::permissions::network_proxy_config_from_profile_network; +use crate::config::permissions::network_proxy_config_for_profile_selection; +use crate::config::permissions::validate_user_permission_profile_names; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; use toml_edit::DocumentMut; @@ -1861,6 +1864,7 @@ impl Config { .permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); + validate_user_permission_profile_names(cfg.permissions.as_ref())?; if has_permission_profiles && !matches!( permission_config_syntax, @@ -1891,8 +1895,7 @@ impl Config { let profiles_are_active = matches!( permission_config_syntax, Some(PermissionConfigSyntax::Profiles) - ) || (permission_config_syntax.is_none() - && has_permission_profiles); + ) || permission_config_syntax.is_none(); let ( configured_network_proxy_config, permission_profile, @@ -1902,24 +1905,19 @@ impl Config { permission_profile.to_runtime_permissions(); let configured_network_proxy_config = if network_sandbox_policy.is_enabled() && profiles_are_active { - let permissions = cfg.permissions.as_ref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a `[permissions]` table", - ) - })?; - let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a named permissions profile", - ) - })?; - let profile = resolve_permission_profile(permissions, default_permissions)?; - // PermissionProfile carries the active network sandbox bit, not the configured // proxy/allowlist policy. Keep that config so active profiles can round-trip // without broadening network behavior. - network_proxy_config_from_profile_network(profile.network.as_ref()) + let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| { + default_builtin_permission_profile_name( + &active_project, + windows_sandbox_level, + ) + }); + network_proxy_config_for_profile_selection( + cfg.permissions.as_ref(), + default_permissions, + )? } else { NetworkProxyConfig::default() }; @@ -1947,32 +1945,27 @@ impl Config { file_system_sandbox_policy, ) } else if profiles_are_active { - let permissions = cfg.permissions.as_ref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a `[permissions]` table", - ) - })?; - let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a named permissions profile", - ) - })?; - let profile = resolve_permission_profile(permissions, default_permissions)?; - let configured_network_proxy_config = - network_proxy_config_from_profile_network(profile.network.as_ref()); + let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| { + default_builtin_permission_profile_name(&active_project, windows_sandbox_level) + }); + let configured_network_proxy_config = network_proxy_config_for_profile_selection( + cfg.permissions.as_ref(), + default_permissions, + )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = - compile_permission_profile( - permissions, + compile_permission_profile_selection( + cfg.permissions.as_ref(), default_permissions, resolved_cwd.as_path(), &mut startup_warnings, )?; - let mut permission_profile = PermissionProfile::from_runtime_permissions( - &file_system_sandbox_policy, - network_sandbox_policy, - ); + let mut permission_profile = builtin_permission_profile(default_permissions) + .unwrap_or_else(|| { + PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ) + }); let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 6d938e9185..f49a61f493 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -12,6 +12,8 @@ use codex_config::permissions_toml::PermissionsToml; use codex_network_proxy::NetworkProxyConfig; #[cfg(test)] use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -20,6 +22,64 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; +use super::ProjectConfig; + +pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = ":read-only"; +pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = ":workspace"; +pub(crate) const BUILT_IN_DANGER_NO_SANDBOX_PROFILE: &str = ":danger-no-sandbox"; + +pub(crate) fn default_builtin_permission_profile_name( + active_project: &ProjectConfig, + windows_sandbox_level: WindowsSandboxLevel, +) -> &'static str { + if (active_project.is_trusted() || active_project.is_untrusted()) + && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) + { + BUILT_IN_WORKSPACE_PROFILE + } else { + BUILT_IN_READ_ONLY_PROFILE + } +} + +pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool { + matches!( + profile_name, + BUILT_IN_READ_ONLY_PROFILE + | BUILT_IN_WORKSPACE_PROFILE + | BUILT_IN_DANGER_NO_SANDBOX_PROFILE + ) +} + +pub(crate) fn builtin_permission_profile(profile_name: &str) -> Option { + match profile_name { + BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()), + BUILT_IN_WORKSPACE_PROFILE => Some(PermissionProfile::workspace_write()), + BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled), + _ => None, + } +} + +pub(crate) fn validate_user_permission_profile_names( + permissions: Option<&PermissionsToml>, +) -> io::Result<()> { + let Some(permissions) = permissions else { + return Ok(()); + }; + + for profile_name in permissions.entries.keys() { + if profile_name.starts_with(':') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "permissions profile `{profile_name}` uses a reserved built-in profile prefix" + ), + )); + } + } + + Ok(()) +} + pub(crate) fn network_proxy_config_from_profile_network( network: Option<&NetworkToml>, ) -> NetworkProxyConfig { @@ -41,6 +101,27 @@ pub(crate) fn resolve_permission_profile<'a>( }) } +pub(crate) fn network_proxy_config_for_profile_selection( + permissions: Option<&PermissionsToml>, + profile_name: &str, +) -> io::Result { + if is_builtin_permission_profile_name(profile_name) { + return Ok(NetworkProxyConfig::default()); + } + reject_unknown_builtin_permission_profile(profile_name)?; + + let permissions = permissions.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "default_permissions requires a `[permissions]` table", + ) + })?; + let profile = resolve_permission_profile(permissions, profile_name)?; + Ok(network_proxy_config_from_profile_network( + profile.network.as_ref(), + )) +} + pub(crate) fn compile_permission_profile( permissions: &PermissionsToml, profile_name: &str, @@ -103,6 +184,37 @@ pub(crate) fn compile_permission_profile( Ok((file_system_sandbox_policy, network_sandbox_policy)) } +pub(crate) fn compile_permission_profile_selection( + permissions: Option<&PermissionsToml>, + profile_name: &str, + policy_cwd: &Path, + startup_warnings: &mut Vec, +) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> { + if let Some(permission_profile) = builtin_permission_profile(profile_name) { + return Ok(permission_profile.to_runtime_permissions()); + } + reject_unknown_builtin_permission_profile(profile_name)?; + + let permissions = permissions.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "default_permissions requires a `[permissions]` table", + ) + })?; + compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings) +} + +fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> { + if profile_name.starts_with(':') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("default_permissions refers to unknown built-in profile `{profile_name}`"), + )); + } + + Ok(()) +} + /// Returns a list of paths that must be readable by shell tools in order /// for Codex to function. These should always be added to the /// `FileSystemSandboxPolicy` for a thread.