From 5c3ca73914cfef7a9317d76cf1cb00ded3cc338a Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Thu, 12 Feb 2026 12:48:36 -0800 Subject: [PATCH 1/4] add a slash command to grant sandbox read access to inaccessible directories (#11512) There is an edge case where a directory is not readable by the sandbox. In practice, we've seen very little of it, but it can happen so this slash command unlocks users when it does. Future idea is to make this a tool that the agent knows about so it can be more integrated. --- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/windows_sandbox.rs | 32 ++++++ .../core/src/windows_sandbox_read_grants.rs | 97 +++++++++++++++++++ codex-rs/tui/src/app.rs | 57 +++++++++++ codex-rs/tui/src/app_event.rs | 13 +++ codex-rs/tui/src/chatwidget.rs | 17 ++++ codex-rs/tui/src/slash_command.rs | 14 ++- codex-rs/windows-sandbox-rs/src/lib.rs | 2 + .../src/setup_orchestrator.rs | 45 ++++++++- 9 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 codex-rs/core/src/windows_sandbox_read_grants.rs diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 6f075e7dc9..764fc4fbba 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -79,6 +79,7 @@ pub mod review_format; pub mod review_prompts; mod thread_manager; pub mod web_search; +pub mod windows_sandbox_read_grants; pub use codex_protocol::protocol::InitialHistory; pub use thread_manager::NewThread; pub use thread_manager::ThreadManager; diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 180cce6c26..02f740ebda 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -10,6 +10,7 @@ use codex_protocol::config_types::WindowsSandboxLevel; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; +use std::path::PathBuf; /// Kill switch for the elevated sandbox NUX on Windows. /// @@ -200,6 +201,25 @@ pub fn run_legacy_setup_preflight( ) } +#[cfg(target_os = "windows")] +pub fn run_setup_refresh_with_extra_read_roots( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + extra_read_roots: Vec, +) -> anyhow::Result<()> { + codex_windows_sandbox::run_setup_refresh_with_extra_read_roots( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + extra_read_roots, + ) +} + #[cfg(not(target_os = "windows"))] pub fn run_legacy_setup_preflight( _policy: &SandboxPolicy, @@ -211,6 +231,18 @@ pub fn run_legacy_setup_preflight( anyhow::bail!("legacy Windows sandbox setup is only supported on Windows") } +#[cfg(not(target_os = "windows"))] +pub fn run_setup_refresh_with_extra_read_roots( + _policy: &SandboxPolicy, + _policy_cwd: &Path, + _command_cwd: &Path, + _env_map: &HashMap, + _codex_home: &Path, + _extra_read_roots: Vec, +) -> anyhow::Result<()> { + anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows") +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/windows_sandbox_read_grants.rs b/codex-rs/core/src/windows_sandbox_read_grants.rs new file mode 100644 index 0000000000..8fa843c8b4 --- /dev/null +++ b/codex-rs/core/src/windows_sandbox_read_grants.rs @@ -0,0 +1,97 @@ +use crate::protocol::SandboxPolicy; +use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots; +use anyhow::Result; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +pub fn grant_read_root_non_elevated( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_root: &Path, +) -> Result { + if !read_root.is_absolute() { + anyhow::bail!("path must be absolute: {}", read_root.display()); + } + if !read_root.exists() { + anyhow::bail!("path does not exist: {}", read_root.display()); + } + if !read_root.is_dir() { + anyhow::bail!("path must be a directory: {}", read_root.display()); + } + + let canonical_root = dunce::canonicalize(read_root)?; + run_setup_refresh_with_extra_read_roots( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + vec![canonical_root.clone()], + )?; + Ok(canonical_root) +} + +#[cfg(test)] +mod tests { + use super::grant_read_root_non_elevated; + use crate::protocol::SandboxPolicy; + use std::collections::HashMap; + use std::path::Path; + use tempfile::TempDir; + + fn policy() -> SandboxPolicy { + SandboxPolicy::new_workspace_write_policy() + } + + #[test] + fn rejects_relative_path() { + let tmp = TempDir::new().expect("tempdir"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + Path::new("relative"), + ) + .expect_err("relative path should fail"); + assert!(err.to_string().contains("path must be absolute")); + } + + #[test] + fn rejects_missing_path() { + let tmp = TempDir::new().expect("tempdir"); + let missing = tmp.path().join("does-not-exist"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + missing.as_path(), + ) + .expect_err("missing path should fail"); + assert!(err.to_string().contains("path does not exist")); + } + + #[test] + fn rejects_file_path() { + let tmp = TempDir::new().expect("tempdir"); + let file_path = tmp.path().join("file.txt"); + std::fs::write(&file_path, "hello").expect("write file"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + file_path.as_path(), + ) + .expect_err("file path should fail"); + assert!(err.to_string().contains("path must be a directory")); + } +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c700a3a846..67d5e30a82 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1826,6 +1826,63 @@ impl App { let _ = preset; } } + AppEvent::BeginWindowsSandboxGrantReadRoot { path } => { + #[cfg(target_os = "windows")] + { + self.chat_widget + .add_to_history(history_cell::new_info_event( + format!("Granting sandbox read access to {path} ..."), + None, + )); + + let policy = self.config.sandbox_policy.get().clone(); + let policy_cwd = self.config.cwd.clone(); + let command_cwd = self.config.cwd.clone(); + let env_map: std::collections::HashMap = + std::env::vars().collect(); + let codex_home = self.config.codex_home.clone(); + let tx = self.app_event_tx.clone(); + + tokio::task::spawn_blocking(move || { + let requested_path = PathBuf::from(path); + let event = match codex_core::windows_sandbox_read_grants::grant_read_root_non_elevated( + &policy, + policy_cwd.as_path(), + command_cwd.as_path(), + &env_map, + codex_home.as_path(), + requested_path.as_path(), + ) { + Ok(canonical_path) => AppEvent::WindowsSandboxGrantReadRootCompleted { + path: canonical_path, + error: None, + }, + Err(err) => AppEvent::WindowsSandboxGrantReadRootCompleted { + path: requested_path, + error: Some(err.to_string()), + }, + }; + tx.send(event); + }); + } + #[cfg(not(target_os = "windows"))] + { + let _ = path; + } + } + AppEvent::WindowsSandboxGrantReadRootCompleted { path, error } => match error { + Some(err) => { + self.chat_widget + .add_to_history(history_cell::new_error_event(format!("Error: {err}"))); + } + None => { + self.chat_widget + .add_to_history(history_cell::new_info_event( + format!("Sandbox read access granted for {}", path.display()), + None, + )); + } + }, AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => { #[cfg(target_os = "windows")] { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index d2168e9619..67db6d7c8b 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -218,6 +218,19 @@ pub(crate) enum AppEvent { preset: ApprovalPreset, }, + /// Begin a non-elevated grant of read access for an additional directory. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + BeginWindowsSandboxGrantReadRoot { + path: String, + }, + + /// Result of attempting to grant read access for an additional directory. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + WindowsSandboxGrantReadRootCompleted { + path: PathBuf, + error: Option, + }, + /// Enable the Windows sandbox feature and switch to Agent mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] EnableWindowsSandboxForAgentMode { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0ae056f836..46dcc19997 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3342,6 +3342,11 @@ impl ChatWidget { // Not supported; on non-Windows this command should never be reachable. }; } + SlashCommand::SandboxReadRoot => { + self.add_error_message( + "Usage: /sandbox-add-read-dir ".to_string(), + ); + } SlashCommand::Experimental => { self.open_experimental_popup(); } @@ -3544,6 +3549,18 @@ impl ChatWidget { }); self.bottom_pane.drain_pending_submission_state(); } + SlashCommand::SandboxReadRoot if !trimmed.is_empty() => { + let Some((prepared_args, _prepared_elements)) = + self.bottom_pane.prepare_inline_args_submission(false) + else { + return; + }; + self.app_event_tx + .send(AppEvent::BeginWindowsSandboxGrantReadRoot { + path: prepared_args, + }); + self.bottom_pane.drain_pending_submission_state(); + } _ => self.dispatch_command(cmd), } } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index a1699c37b8..16e767f802 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -17,6 +17,8 @@ pub enum SlashCommand { Permissions, #[strum(serialize = "setup-default-sandbox")] ElevateSandbox, + #[strum(serialize = "sandbox-add-read-dir")] + SandboxReadRoot, Experimental, Skills, Review, @@ -84,7 +86,10 @@ impl SlashCommand { SlashCommand::Agent => "switch the active agent thread", SlashCommand::Approvals => "choose what Codex is allowed to do", SlashCommand::Permissions => "choose what Codex is allowed to do", - SlashCommand::ElevateSandbox => "set up default agent sandbox", + SlashCommand::ElevateSandbox => "set up elevated agent sandbox", + SlashCommand::SandboxReadRoot => { + "let sandbox read a directory: /sandbox-add-read-dir " + } SlashCommand::Experimental => "toggle experimental features", SlashCommand::Mcp => "list configured MCP tools", SlashCommand::Apps => "manage apps", @@ -104,7 +109,10 @@ impl SlashCommand { pub fn supports_inline_args(self) -> bool { matches!( self, - SlashCommand::Review | SlashCommand::Rename | SlashCommand::Plan + SlashCommand::Review + | SlashCommand::Rename + | SlashCommand::Plan + | SlashCommand::SandboxReadRoot ) } @@ -122,6 +130,7 @@ impl SlashCommand { | SlashCommand::Approvals | SlashCommand::Permissions | SlashCommand::ElevateSandbox + | SlashCommand::SandboxReadRoot | SlashCommand::Experimental | SlashCommand::Review | SlashCommand::Plan @@ -151,6 +160,7 @@ impl SlashCommand { fn is_visible(self) -> bool { match self { + SlashCommand::SandboxReadRoot => cfg!(target_os = "windows"), SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions), _ => true, } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 3e15c7a272..61077342dc 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -83,6 +83,8 @@ pub use setup::run_elevated_setup; #[cfg(target_os = "windows")] pub use setup::run_setup_refresh; #[cfg(target_os = "windows")] +pub use setup::run_setup_refresh_with_extra_read_roots; +#[cfg(target_os = "windows")] pub use setup::sandbox_dir; #[cfg(target_os = "windows")] pub use setup::sandbox_secrets_dir; diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 0e43d1ba4b..e7f9bee69f 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -61,6 +61,47 @@ pub fn run_setup_refresh( command_cwd: &Path, env_map: &HashMap, codex_home: &Path, +) -> Result<()> { + run_setup_refresh_inner( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + None, + None, + ) +} + +pub fn run_setup_refresh_with_extra_read_roots( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + extra_read_roots: Vec, +) -> Result<()> { + let mut read_roots = gather_read_roots(command_cwd, policy); + read_roots.extend(extra_read_roots); + run_setup_refresh_inner( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + Some(read_roots), + Some(Vec::new()), + ) +} + +fn run_setup_refresh_inner( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_roots_override: Option>, + write_roots_override: Option>, ) -> Result<()> { // Skip in danger-full-access. if matches!( @@ -75,8 +116,8 @@ pub fn run_setup_refresh( command_cwd, env_map, codex_home, - None, - None, + read_roots_override, + write_roots_override, ); let payload = ElevationPayload { version: SETUP_VERSION, From 4668feb43a7ab2becf357765c538c70a2043b4e1 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Thu, 12 Feb 2026 13:23:30 -0800 Subject: [PATCH 2/4] chore(core) Deprecate approval_policy: on-failure (#11631) ## Summary In an effort to start simplifying our sandbox setup, we're announcing this approval_policy as deprecated. In general, it performs worse than `on-request`, and we're focusing on making fewer sandbox configurations perform much better. ## Testing - [x] Tested locally - [x] Existing tests pass --- .../app-server-protocol/schema/json/ClientRequest.json | 2 +- codex-rs/app-server-protocol/schema/json/EventMsg.json | 2 +- .../schema/json/ServerNotification.json | 2 +- .../schema/json/codex_app_server_protocol.schemas.json | 2 +- .../schema/json/v1/ForkConversationParams.json | 2 +- .../schema/json/v1/ForkConversationResponse.json | 2 +- .../schema/json/v1/GetUserSavedConfigResponse.json | 2 +- .../schema/json/v1/NewConversationParams.json | 2 +- .../schema/json/v1/ResumeConversationParams.json | 2 +- .../schema/json/v1/ResumeConversationResponse.json | 2 +- .../schema/json/v1/SendUserTurnParams.json | 2 +- .../schema/json/v1/SessionConfiguredNotification.json | 2 +- codex-rs/core/config.schema.json | 2 +- codex-rs/core/src/codex.rs | 8 ++++++++ codex-rs/debug-client/README.md | 2 +- codex-rs/docs/codex_mcp_interface.md | 2 +- codex-rs/protocol/src/protocol.rs | 10 ++++++---- codex-rs/utils/cli/src/approval_mode_cli_arg.rs | 3 ++- 18 files changed, 31 insertions(+), 20 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a63f11bccd..c7a3504fba 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -88,7 +88,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index c0a5028996..6af66294c8 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -104,7 +104,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 1b312ea1e0..05667704d7 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -241,7 +241,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], 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 e124325f01..5820997fb6 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 @@ -208,7 +208,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json index bc84249516..cd52a492e5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json @@ -12,7 +12,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index 7d0d26c052..12db241e1a 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -104,7 +104,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json b/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json index b5e472b6ce..beec3a22cc 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json @@ -16,7 +16,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json index 167aee56e9..327ecd23b2 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json @@ -12,7 +12,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json index 5c7f943786..9f14c22084 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json @@ -12,7 +12,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index 4b1e908158..dd7606b5e0 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -104,7 +104,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json index b2fc248663..aa112e7319 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json @@ -16,7 +16,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index c9fb2c8e38..89ab825d32 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -104,7 +104,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 5fb67c22b4..4c571a6c2e 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -100,7 +100,7 @@ "type": "string" }, { - "description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.", + "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", "enum": [ "on-failure" ], diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 647b2f7609..7f15d3226b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1099,6 +1099,14 @@ impl Session { }); } maybe_push_unstable_features_warning(&config, &mut post_session_configured_events); + if config.approval_policy.value() == AskForApproval::OnFailure { + post_session_configured_events.push(Event { + id: "".to_owned(), + msg: EventMsg::Warning(WarningEvent { + message: "`on-failure` approval policy is deprecated and will be removed in a future release. Use `on-request` for interactive approvals or `never` for non-interactive runs.".to_string(), + }), + }); + } let auth = auth.as_ref(); let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from); diff --git a/codex-rs/debug-client/README.md b/codex-rs/debug-client/README.md index 091fc1e6aa..bd310d9056 100644 --- a/codex-rs/debug-client/README.md +++ b/codex-rs/debug-client/README.md @@ -26,7 +26,7 @@ cargo run -p codex-debug-client -- --thread-id thr_123 - `--codex-bin `: path to the `codex` binary (default: `codex`). - `-c, --config key=value`: pass through `--config` overrides to `codex`. - `--thread-id `: resume a thread instead of starting a new one. -- `--approval-policy `: `untrusted`, `on-failure`, `on-request`, `never`. +- `--approval-policy `: `untrusted`, `on-failure` (deprecated), `on-request`, `never`. - `--auto-approve`: auto-approve command/file-change approvals (default: decline). - `--final-only`: only show completed assistant messages and tool items. - `--model `: optional model override for thread start/resume. diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index da415d9deb..293ecb7ecf 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -59,7 +59,7 @@ Request `newConversation` params (subset): - `model`: string model id (e.g. "o3", "gpt-5.1", "gpt-5.1-codex") - `profile`: optional named profile - `cwd`: optional working directory -- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` | `never` +- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` (deprecated) | `never` - `sandbox`: `read-only` | `workspace-write` | `external-sandbox` (honors `networkAccess` restricted/enabled) | `danger-full-access` - `config`: map of additional config overrides - `baseInstructions`: optional instruction override diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6097c60e8a..745068b16e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -356,10 +356,12 @@ pub enum AskForApproval { #[strum(serialize = "untrusted")] UnlessTrusted, - /// *All* commands are auto‑approved, but they are expected to run inside a - /// sandbox where network access is disabled and writes are confined to a - /// specific set of paths. If the command fails, it will be escalated to - /// the user to approve execution without a sandbox. + /// DEPRECATED: *All* commands are auto‑approved, but they are expected to + /// run inside a sandbox where network access is disabled and writes are + /// confined to a specific set of paths. If the command fails, it will be + /// escalated to the user to approve execution without a sandbox. + /// Prefer `OnRequest` for interactive runs or `Never` for non-interactive + /// runs. OnFailure, /// The model decides when to ask the user for approval. diff --git a/codex-rs/utils/cli/src/approval_mode_cli_arg.rs b/codex-rs/utils/cli/src/approval_mode_cli_arg.rs index 2ba1e3dc59..08b7be6cca 100644 --- a/codex-rs/utils/cli/src/approval_mode_cli_arg.rs +++ b/codex-rs/utils/cli/src/approval_mode_cli_arg.rs @@ -12,9 +12,10 @@ pub enum ApprovalModeCliArg { /// is not in the "trusted" set. Untrusted, - /// Run all commands without asking for user approval. + /// DEPRECATED: Run all commands without asking for user approval. /// Only asks for approval if a command fails to execute, in which case it /// will escalate to the user to ask for un-sandboxed execution. + /// Prefer `on-request` for interactive runs or `never` for non-interactive runs. OnFailure, /// The model decides when to ask the user for approval. From d7cb70ed267ee798a3988812a1a3c788fb006f65 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Thu, 12 Feb 2026 14:10:30 -0800 Subject: [PATCH 3/4] Better error message for model limit hit. (#11636) image --- codex-rs/core/src/api_bridge.rs | 46 +++++++++++++++++++++++++++++++-- codex-rs/core/src/error.rs | 34 ++++++++++-------------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/src/api_bridge.rs b/codex-rs/core/src/api_bridge.rs index b461ac4ad9..3b2024f58b 100644 --- a/codex-rs/core/src/api_bridge.rs +++ b/codex-rs/core/src/api_bridge.rs @@ -81,7 +81,6 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr { resets_at, rate_limits: rate_limits.map(Box::new), promo_message, - limit_name: limit_id, }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; @@ -156,6 +155,10 @@ mod tests { ACTIVE_LIMIT_HEADER, http::HeaderValue::from_static("codex_other"), ); + headers.insert( + "x-codex-other-limit-name", + http::HeaderValue::from_static("codex_other"), + ); let body = serde_json::json!({ "error": { "type": "usage_limit_reached", @@ -173,7 +176,46 @@ mod tests { let CodexErr::UsageLimitReached(usage_limit) = err else { panic!("expected CodexErr::UsageLimitReached, got {err:?}"); }; - assert_eq!(usage_limit.limit_name.as_deref(), Some("codex_other")); + assert_eq!( + usage_limit + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()), + Some("codex_other") + ); + } + + #[test] + fn map_api_error_does_not_fallback_limit_name_to_limit_id() { + let mut headers = HeaderMap::new(); + headers.insert( + ACTIVE_LIMIT_HEADER, + http::HeaderValue::from_static("codex_other"), + ); + let body = serde_json::json!({ + "error": { + "type": "usage_limit_reached", + "plan_type": "pro", + } + }) + .to_string(); + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::TOO_MANY_REQUESTS, + url: Some("http://example.com/v1/responses".to_string()), + headers: Some(headers), + body: Some(body), + })); + + let CodexErr::UsageLimitReached(usage_limit) = err else { + panic!("expected CodexErr::UsageLimitReached, got {err:?}"); + }; + assert_eq!( + usage_limit + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()), + None + ); } } diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index c7d9ffb82d..404fbc5a7a 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -411,18 +411,22 @@ pub struct UsageLimitReachedError { pub(crate) resets_at: Option>, pub(crate) rate_limits: Option>, pub(crate) promo_message: Option, - pub(crate) limit_name: Option, } impl std::fmt::Display for UsageLimitReachedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(limit_name) = self.limit_name.as_deref() + if let Some(limit_name) = self + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()) + .map(str::trim) + .filter(|name| !name.is_empty()) && !limit_name.eq_ignore_ascii_case("codex") { return write!( f, - "You've hit your usage limit for {limit_name}.{}", - retry_suffix(self.resets_at.as_ref()) + "You've hit your usage limit for {limit_name}. Switch to another model now,{}", + retry_suffix_after_or(self.resets_at.as_ref()) ); } @@ -704,7 +708,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -822,7 +825,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -837,7 +839,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -852,7 +853,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -871,7 +871,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!( "You've hit your usage limit. To get more access now, send a request to your admin or try again at {expected_time}." @@ -887,7 +886,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -902,7 +900,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -921,7 +918,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!( "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." @@ -939,15 +935,18 @@ mod tests { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Plus)), resets_at: Some(resets_at), - rate_limits: Some(Box::new(rate_limit_snapshot())), + rate_limits: Some(Box::new(RateLimitSnapshot { + limit_id: Some("codex_other".to_string()), + limit_name: Some("codex_other".to_string()), + ..rate_limit_snapshot() + })), promo_message: Some( "Visit https://chatgpt.com/codex/settings/usage to purchase more credits" .to_string(), ), - limit_name: Some("codex_other".to_string()), }; let expected = format!( - "You've hit your usage limit for codex_other. Try again at {expected_time}." + "You've hit your usage limit for codex_other. Switch to another model now, or try again at {expected_time}." ); assert_eq!(err.to_string(), expected); }); @@ -964,7 +963,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1074,7 +1072,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!( "You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." @@ -1095,7 +1092,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1113,7 +1109,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1133,7 +1128,6 @@ mod tests { promo_message: Some( "To continue using Codex, start a free trial of today".to_string(), ), - limit_name: None, }; let expected = format!( "You've hit your usage limit. To continue using Codex, start a free trial of today, or try again at {expected_time}." From 5032154ecfa1b2bcf00d978b4d07c4293193a764 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 12 Feb 2026 14:15:45 -0800 Subject: [PATCH 4/4] feat: introduce EffectivePermissions --- .../app-server/src/codex_message_processor.rs | 8 +- codex-rs/cli/src/debug_sandbox.rs | 10 +- codex-rs/core/src/agent/role.rs | 1 + codex-rs/core/src/codex.rs | 34 ++--- codex-rs/core/src/config/mod.rs | 142 +++++++++++------- codex-rs/core/src/memories/dispatch.rs | 4 +- codex-rs/core/src/tasks/review.rs | 2 +- codex-rs/core/src/tools/handlers/collab.rs | 17 ++- codex-rs/core/src/windows_sandbox.rs | 2 +- codex-rs/core/tests/suite/approvals.rs | 12 +- codex-rs/core/tests/suite/client.rs | 4 +- codex-rs/core/tests/suite/codex_delegate.rs | 10 +- .../tests/suite/collaboration_instructions.rs | 8 +- codex-rs/core/tests/suite/otel.rs | 23 ++- codex-rs/core/tests/suite/override_updates.rs | 2 +- .../core/tests/suite/permissions_messages.rs | 18 +-- codex-rs/core/tests/suite/personality.rs | 20 +-- codex-rs/core/tests/suite/prompt_caching.rs | 8 +- codex-rs/core/tests/suite/remote_models.rs | 4 +- codex-rs/core/tests/suite/resume_warning.rs | 4 +- codex-rs/core/tests/suite/tools.rs | 4 +- codex-rs/exec/src/lib.rs | 4 +- codex-rs/tui/src/app.rs | 21 +-- codex-rs/tui/src/chatwidget.rs | 32 ++-- codex-rs/tui/src/chatwidget/tests.rs | 34 ++++- codex-rs/tui/src/debug_config.rs | 1 + codex-rs/tui/src/lib.rs | 12 +- codex-rs/tui/src/status/card.rs | 18 ++- codex-rs/tui/src/status/tests.rs | 3 + .../sandbox-summary/src/config_summary.rs | 7 +- 30 files changed, 278 insertions(+), 191 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 603f34b016..aaffa266f0 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1677,11 +1677,11 @@ impl CodexMessageProcessor { } let cwd = params.cwd.unwrap_or_else(|| self.config.cwd.clone()); - let env = create_env(&self.config.shell_environment_policy, None); + let env = create_env(&self.config.permissions.shell_environment_policy, None); let timeout_ms = params .timeout_ms .and_then(|timeout_ms| u64::try_from(timeout_ms).ok()); - let started_network_proxy = match self.config.network.as_ref() { + let started_network_proxy = match self.config.permissions.network.as_ref() { Some(spec) => match spec.start_proxy().await { Ok(started) => Some(started), Err(err) => { @@ -1713,7 +1713,7 @@ impl CodexMessageProcessor { let requested_policy = params.sandbox_policy.map(|policy| policy.to_core()); let effective_policy = match requested_policy { - Some(policy) => match self.config.sandbox_policy.can_set(&policy) { + Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) { Ok(()) => policy, Err(err) => { let error = JSONRPCErrorError { @@ -1725,7 +1725,7 @@ impl CodexMessageProcessor { return; } }, - None => self.config.sandbox_policy.get().clone(), + None => self.config.permissions.sandbox_policy.get().clone(), }; let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone(); diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index ca4e9999d0..c19d58189f 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -130,7 +130,7 @@ async fn run_command_under_sandbox( let sandbox_policy_cwd = cwd.clone(); let stdio_policy = StdioPolicy::Inherit; - let env = create_env(&config.shell_environment_policy, None); + let env = create_env(&config.permissions.shell_environment_policy, None); // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio. if let SandboxType::Windows = sandbox_type { @@ -141,7 +141,7 @@ async fn run_command_under_sandbox( use codex_windows_sandbox::run_windows_sandbox_capture; use codex_windows_sandbox::run_windows_sandbox_capture_elevated; - let policy_str = serde_json::to_string(config.sandbox_policy.get())?; + let policy_str = serde_json::to_string(config.permissions.sandbox_policy.get())?; let sandbox_cwd = sandbox_policy_cwd.clone(); let cwd_clone = cwd.clone(); @@ -214,7 +214,7 @@ async fn run_command_under_sandbox( let _ = log_denials; // This proxy should only live for the lifetime of the child process. - let network_proxy = match config.network.as_ref() { + let network_proxy = match config.permissions.network.as_ref() { Some(spec) => Some( spec.start_proxy() .await @@ -232,7 +232,7 @@ async fn run_command_under_sandbox( spawn_command_under_seatbelt( command, cwd, - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), sandbox_policy_cwd.as_path(), stdio_policy, network.as_ref(), @@ -251,7 +251,7 @@ async fn run_command_under_sandbox( codex_linux_sandbox_exe, command, cwd, - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), sandbox_policy_cwd.as_path(), use_bwrap_sandbox, stdio_policy, diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 74eca4179d..1dcf8b679e 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -122,6 +122,7 @@ Rules: } if profile.read_only { config + .permissions .sandbox_policy .set(SandboxPolicy::new_read_only_policy()) .map_err(|err| format!("sandbox_policy is invalid: {err}"))?; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 7f15d3226b..f1edc474c5 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -389,8 +389,8 @@ impl Codex { personality: config.personality, base_instructions, compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -932,7 +932,7 @@ impl Session { sandbox_policy: session_configuration.sandbox_policy.get().clone(), network, windows_sandbox_level: session_configuration.windows_sandbox_level, - shell_environment_policy: per_turn_config.shell_environment_policy.clone(), + shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), tools_config, features: per_turn_config.features.clone(), ghost_snapshot: per_turn_config.ghost_snapshot.clone(), @@ -1142,8 +1142,8 @@ impl Session { config.model_reasoning_summary, config.model_context_window, config.model_auto_compact_token_limit, - config.approval_policy.value(), - config.sandbox_policy.get().clone(), + config.permissions.approval_policy.value(), + config.permissions.sandbox_policy.get().clone(), mcp_servers.keys().map(String::as_str).collect(), config.active_profile.clone(), ); @@ -1175,7 +1175,7 @@ impl Session { session_configuration.thread_name = thread_name.clone(); let mut state = SessionState::new(session_configuration.clone()); let network_proxy = - match config.network.as_ref() { + match config.permissions.network.as_ref() { Some(spec) => Some(spec.start_proxy().await.map_err(|err| { anyhow::anyhow!("failed to start managed network proxy: {err}") })?), @@ -1697,7 +1697,7 @@ impl Session { if sandbox_policy_changed { let sandbox_state = SandboxState { - sandbox_policy: per_turn_config.sandbox_policy.get().clone(), + sandbox_policy: per_turn_config.permissions.sandbox_policy.get().clone(), codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), sandbox_cwd: per_turn_config.cwd.clone(), use_linux_sandbox_bwrap: per_turn_config @@ -6312,8 +6312,8 @@ mod tests { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -6403,8 +6403,8 @@ mod tests { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -6713,8 +6713,8 @@ mod tests { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -6766,8 +6766,8 @@ mod tests { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -6912,8 +6912,8 @@ mod tests { .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy.clone(), - sandbox_policy: config.sandbox_policy.clone(), + approval_policy: config.permissions.approval_policy.clone(), + sandbox_policy: config.permissions.sandbox_policy.clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 28b619eed7..d5de27a40d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -117,6 +117,22 @@ pub(crate) fn test_config() -> Config { .expect("load default test config") } +/// Application configuration loaded from disk and merged with overrides. +#[derive(Debug, Clone, PartialEq)] +pub struct Permissions { + /// Approval policy for executing commands. + pub approval_policy: Constrained, + /// Effective sandbox policy used for shell/unified exec. + pub sandbox_policy: Constrained, + /// Effective network configuration applied to all spawned processes. + pub network: Option, + /// Policy used to build process environments for shell/unified exec. + pub shell_environment_policy: ShellEnvironmentPolicy, + /// Effective Windows sandbox mode derived from `[windows].sandbox` or + /// legacy feature keys. + pub windows_sandbox_mode: Option, +} + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { @@ -148,25 +164,18 @@ pub struct Config { /// Optionally specify the personality of the model pub personality: Option, - /// Approval policy for executing commands. - pub approval_policy: Constrained, - - pub sandbox_policy: Constrained, + /// Effective permission configuration for shell tool execution. + pub permissions: Permissions, /// enforce_residency means web traffic cannot be routed outside of a /// particular geography. HTTP clients should direct their requests /// using backend-specific headers or URLs to enforce this. pub enforce_residency: Constrained>, - /// Effective network configuration applied to all spawned processes. - pub network: Option, - /// True if the user passed in an override or set a value in config.toml /// for either of approval_policy or sandbox_mode. pub did_user_set_custom_approval_policy_or_sandbox_mode: bool, - pub shell_environment_policy: ShellEnvironmentPolicy, - /// When `true`, `AgentReasoning` events emitted by the backend will be /// suppressed from the frontend output. This can reduce visual noise when /// users are only interested in the final agent responses. @@ -345,10 +354,6 @@ pub struct Config { /// Settings for ghost snapshots (used for undo). pub ghost_snapshot: GhostSnapshotConfig, - /// Effective Windows sandbox mode derived from `[windows].sandbox` or - /// legacy feature keys. - pub windows_sandbox_mode: Option, - /// Centralized feature flags; source of truth for feature gating. pub features: Features, @@ -1726,12 +1731,15 @@ impl Config { model_provider, cwd: resolved_cwd, startup_warnings, - approval_policy: constrained_approval_policy.value, - sandbox_policy: constrained_sandbox_policy.value, + permissions: Permissions { + approval_policy: constrained_approval_policy.value, + sandbox_policy: constrained_sandbox_policy.value, + network, + shell_environment_policy, + windows_sandbox_mode, + }, enforce_residency: enforce_residency.value, - network, did_user_set_custom_approval_policy_or_sandbox_mode, - shell_environment_policy, notify: cfg.notify, user_instructions, base_instructions, @@ -1796,7 +1804,6 @@ impl Config { web_search_mode: constrained_web_search_mode.value, use_experimental_unified_exec_tool, ghost_snapshot, - windows_sandbox_mode, features, suppress_unstable_features_warning: cfg .suppress_unstable_features_warning @@ -1902,28 +1909,28 @@ impl Config { } pub fn set_windows_sandbox_enabled(&mut self, value: bool) { - self.windows_sandbox_mode = if value { + self.permissions.windows_sandbox_mode = if value { Some(WindowsSandboxModeToml::Unelevated) } else if matches!( - self.windows_sandbox_mode, + self.permissions.windows_sandbox_mode, Some(WindowsSandboxModeToml::Unelevated) ) { None } else { - self.windows_sandbox_mode + self.permissions.windows_sandbox_mode }; } pub fn set_windows_elevated_sandbox_enabled(&mut self, value: bool) { - self.windows_sandbox_mode = if value { + self.permissions.windows_sandbox_mode = if value { Some(WindowsSandboxModeToml::Elevated) } else if matches!( - self.windows_sandbox_mode, + self.permissions.windows_sandbox_mode, Some(WindowsSandboxModeToml::Elevated) ) { None } else { - self.windows_sandbox_mode + self.permissions.windows_sandbox_mode }; } } @@ -2366,12 +2373,12 @@ trust_level = "trusted" let expected_backend = AbsolutePathBuf::try_from(backend).unwrap(); if cfg!(target_os = "windows") { - match config.sandbox_policy.get() { + match config.permissions.sandbox_policy.get() { SandboxPolicy::ReadOnly { .. } => {} other => panic!("expected read-only policy on Windows, got {other:?}"), } } else { - match config.sandbox_policy.get() { + match config.permissions.sandbox_policy.get() { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { assert_eq!( writable_roots @@ -2660,7 +2667,7 @@ profile = "project" )?; assert!(matches!( - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), &SandboxPolicy::DangerFullAccess )); assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode); @@ -2698,12 +2705,12 @@ profile = "project" if cfg!(target_os = "windows") { assert!(matches!( - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), SandboxPolicy::ReadOnly { .. } )); } else { assert!(matches!( - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), SandboxPolicy::WorkspaceWrite { .. } )); } @@ -4019,12 +4026,15 @@ model_verbosity = "high" model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: Constrained::allow_any(AskForApproval::Never), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permissions: Permissions { + approval_policy: Constrained::allow_any(AskForApproval::Never), + sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + network: None, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + }, enforce_residency: Constrained::allow_any(None), - network: None, did_user_set_custom_approval_policy_or_sandbox_mode: true, - shell_environment_policy: ShellEnvironmentPolicy::default(), user_instructions: None, notify: None, cwd: fixture.cwd(), @@ -4067,7 +4077,6 @@ model_verbosity = "high" suppress_unstable_features_warning: false, active_profile: Some("o3".to_string()), active_project: ProjectConfig { trust_level: None }, - windows_sandbox_mode: None, windows_wsl_setup_acknowledged: false, notices: Default::default(), check_for_update_on_startup: true, @@ -4126,12 +4135,15 @@ model_verbosity = "high" model_auto_compact_token_limit: None, model_provider_id: "openai-custom".to_string(), model_provider: fixture.openai_custom_provider.clone(), - approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permissions: Permissions { + approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), + sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + network: None, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + }, enforce_residency: Constrained::allow_any(None), - network: None, did_user_set_custom_approval_policy_or_sandbox_mode: true, - shell_environment_policy: ShellEnvironmentPolicy::default(), user_instructions: None, notify: None, cwd: fixture.cwd(), @@ -4174,7 +4186,6 @@ model_verbosity = "high" suppress_unstable_features_warning: false, active_profile: Some("gpt3".to_string()), active_project: ProjectConfig { trust_level: None }, - windows_sandbox_mode: None, windows_wsl_setup_acknowledged: false, notices: Default::default(), check_for_update_on_startup: true, @@ -4231,12 +4242,15 @@ model_verbosity = "high" model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: Constrained::allow_any(AskForApproval::OnFailure), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permissions: Permissions { + approval_policy: Constrained::allow_any(AskForApproval::OnFailure), + sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + network: None, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + }, enforce_residency: Constrained::allow_any(None), - network: None, did_user_set_custom_approval_policy_or_sandbox_mode: true, - shell_environment_policy: ShellEnvironmentPolicy::default(), user_instructions: None, notify: None, cwd: fixture.cwd(), @@ -4279,7 +4293,6 @@ model_verbosity = "high" suppress_unstable_features_warning: false, active_profile: Some("zdr".to_string()), active_project: ProjectConfig { trust_level: None }, - windows_sandbox_mode: None, windows_wsl_setup_acknowledged: false, notices: Default::default(), check_for_update_on_startup: true, @@ -4322,12 +4335,15 @@ model_verbosity = "high" model_auto_compact_token_limit: None, model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: Constrained::allow_any(AskForApproval::OnFailure), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permissions: Permissions { + approval_policy: Constrained::allow_any(AskForApproval::OnFailure), + sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + network: None, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + }, enforce_residency: Constrained::allow_any(None), - network: None, did_user_set_custom_approval_policy_or_sandbox_mode: true, - shell_environment_policy: ShellEnvironmentPolicy::default(), user_instructions: None, notify: None, cwd: fixture.cwd(), @@ -4370,7 +4386,6 @@ model_verbosity = "high" suppress_unstable_features_warning: false, active_profile: Some("gpt5".to_string()), active_project: ProjectConfig { trust_level: None }, - windows_sandbox_mode: None, windows_wsl_setup_acknowledged: false, notices: Default::default(), check_for_update_on_startup: true, @@ -4903,7 +4918,7 @@ mcp_oauth_callback_port = 5678 // Verify that untrusted projects get UnlessTrusted approval policy assert_eq!( - config.approval_policy.value(), + config.permissions.approval_policy.value(), AskForApproval::UnlessTrusted, "Expected UnlessTrusted approval policy for untrusted project" ); @@ -4911,13 +4926,16 @@ mcp_oauth_callback_port = 5678 // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) if cfg!(target_os = "windows") { assert!( - matches!(config.sandbox_policy.get(), SandboxPolicy::ReadOnly { .. }), + matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::ReadOnly { .. } + ), "Expected ReadOnly on Windows" ); } else { assert!( matches!( - config.sandbox_policy.get(), + config.permissions.sandbox_policy.get(), SandboxPolicy::WorkspaceWrite { .. } ), "Expected WorkspaceWrite sandbox for untrusted project" @@ -4944,9 +4962,8 @@ mcp_oauth_callback_port = 5678 })) .build() .await?; - assert_eq!( - *config.sandbox_policy.get(), + *config.permissions.sandbox_policy.get(), SandboxPolicy::new_read_only_policy() ); Ok(()) @@ -4983,7 +5000,7 @@ mcp_oauth_callback_port = 5678 .build() .await?; assert_eq!( - *config.sandbox_policy.get(), + *config.permissions.sandbox_policy.get(), SandboxPolicy::new_read_only_policy() ); Ok(()) @@ -5015,7 +5032,10 @@ mcp_oauth_callback_port = 5678 assert_eq!(config.web_search_mode.value(), WebSearchMode::Cached); assert_eq!( - resolve_web_search_mode_for_turn(&config.web_search_mode, config.sandbox_policy.get()), + resolve_web_search_mode_for_turn( + &config.web_search_mode, + config.permissions.sandbox_policy.get(), + ), WebSearchMode::Cached, ); Ok(()) @@ -5049,7 +5069,10 @@ trust_level = "untrusted" .build() .await?; - assert_eq!(config.approval_policy.value(), AskForApproval::OnRequest); + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::OnRequest + ); Ok(()) } @@ -5074,7 +5097,10 @@ trust_level = "untrusted" })) .build() .await?; - assert_eq!(config.approval_policy.value(), AskForApproval::OnRequest); + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::OnRequest + ); Ok(()) } } diff --git a/codex-rs/core/src/memories/dispatch.rs b/codex-rs/core/src/memories/dispatch.rs index ce7b8f07c2..bfa6daa5a8 100644 --- a/codex-rs/core/src/memories/dispatch.rs +++ b/codex-rs/core/src/memories/dispatch.rs @@ -95,7 +95,8 @@ pub(in crate::memories) async fn run_global_memory_consolidation( let consolidation_config = { let mut consolidation_config = config.as_ref().clone(); consolidation_config.cwd = root.clone(); - consolidation_config.approval_policy = Constrained::allow_only(AskForApproval::Never); + consolidation_config.permissions.approval_policy = + Constrained::allow_only(AskForApproval::Never); let mut writable_roots = Vec::new(); match AbsolutePathBuf::from_absolute_path(consolidation_config.codex_home.clone()) { Ok(codex_home) => writable_roots.push(codex_home), @@ -112,6 +113,7 @@ pub(in crate::memories) async fn run_global_memory_consolidation( exclude_slash_tmp: false, }; if let Err(err) = consolidation_config + .permissions .sandbox_policy .set(consolidation_sandbox_policy) { diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 4082c0f8e3..a2fd729850 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -97,7 +97,7 @@ async fn start_review_conversation( // Set explicit review rubric for the sub-agent sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); - sub_agent_config.approval_policy = Constrained::allow_only(AskForApproval::Never); + sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); let model = config .review_model diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index 07a21a3fcd..8e5f23450c 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -818,11 +818,12 @@ fn build_agent_shared_config( config.model_reasoning_summary = turn.reasoning_summary; config.developer_instructions = turn.developer_instructions.clone(); config.compact_prompt = turn.compact_prompt.clone(); - config.shell_environment_policy = turn.shell_environment_policy.clone(); + config.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); config.cwd = turn.cwd.clone(); - config.approval_policy = Constrained::allow_only(AskForApproval::Never); + config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); config + .permissions .sandbox_policy .set(turn.sandbox_policy.clone()) .map_err(|err| { @@ -1717,8 +1718,8 @@ mod tests { turn.cwd = temp_dir.path().to_path_buf(); turn.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo")); turn.sandbox_policy = pick_allowed_sandbox_policy( - &turn.config.sandbox_policy, - turn.config.sandbox_policy.get().clone(), + &turn.config.permissions.sandbox_policy, + turn.config.permissions.sandbox_policy.get().clone(), ); let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config"); @@ -1730,14 +1731,16 @@ mod tests { expected.model_reasoning_summary = turn.reasoning_summary; expected.developer_instructions = turn.developer_instructions.clone(); expected.compact_prompt = turn.compact_prompt.clone(); - expected.shell_environment_policy = turn.shell_environment_policy.clone(); + expected.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); expected.cwd = turn.cwd.clone(); expected + .permissions .approval_policy .set(AskForApproval::Never) .expect("approval policy set"); expected + .permissions .sandbox_policy .set(turn.sandbox_policy) .expect("sandbox policy set"); @@ -1777,14 +1780,16 @@ mod tests { expected.model_reasoning_summary = turn.reasoning_summary; expected.developer_instructions = turn.developer_instructions.clone(); expected.compact_prompt = turn.compact_prompt.clone(); - expected.shell_environment_policy = turn.shell_environment_policy.clone(); + expected.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); expected.cwd = turn.cwd.clone(); expected + .permissions .approval_policy .set(AskForApproval::Never) .expect("approval policy set"); expected + .permissions .sandbox_policy .set(turn.sandbox_policy) .expect("sandbox policy set"); diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 02f740ebda..363bec3c7e 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -25,7 +25,7 @@ pub trait WindowsSandboxLevelExt { impl WindowsSandboxLevelExt for WindowsSandboxLevel { fn from_config(config: &Config) -> WindowsSandboxLevel { - match config.windows_sandbox_mode { + match config.permissions.windows_sandbox_mode { Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated, Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken, None => Self::from_features(&config.features), diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 3b5e115ef8..7683339ceb 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1467,8 +1467,8 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> { let model = model_override.unwrap_or("gpt-5.1"); let mut builder = test_codex().with_model(model).with_config(move |config| { - config.approval_policy = Constrained::allow_any(approval_policy); - config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone()); + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy.clone()); for feature in features { config.features.enable(feature); } @@ -1585,8 +1585,8 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() let mut builder = test_codex() .with_model("gpt-5.1-codex") .with_config(move |config| { - config.approval_policy = Constrained::allow_any(approval_policy); - config.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); }); let test = builder.build(&server).await?; @@ -1692,8 +1692,8 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts let sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { - config.approval_policy = Constrained::allow_any(approval_policy); - config.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); }); let test = builder.build(&server).await?; let allow_prefix_path = test.cwd.path().join("allow-prefix.txt"); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index ed6912daae..7c6799df92 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -973,8 +973,8 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re text_elements: Vec::new(), }], cwd: config.cwd.clone(), - approval_policy: config.approval_policy.value(), - sandbox_policy: config.sandbox_policy.get().clone(), + approval_policy: config.permissions.approval_policy.value(), + sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: session_configured.model.clone(), effort: Some(ReasoningEffort::Low), summary: config.model_reasoning_summary, diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index a7a8bc7eec..0cbdcef06e 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -63,8 +63,9 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { // Build a conversation configured to require approvals so the delegate // routes ExecApprovalRequest via the parent. let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.sandbox_policy = + Constrained::allow_any(SandboxPolicy::new_read_only_policy()); }); let test = builder.build(&server).await.expect("build test codex"); @@ -144,9 +145,10 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { mount_sse_sequence(&server, vec![sse1, sse2]).await; let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); // Use a restricted sandbox so patch approval is required - config.sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); + config.permissions.sandbox_policy = + Constrained::allow_any(SandboxPolicy::new_read_only_policy()); config.include_apply_patch_tool = true; }); let test = builder.build(&server).await.expect("build test codex"); diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 160866819a..a6cad3a5ce 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -165,8 +165,8 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { text_elements: Vec::new(), }], cwd: test.config.cwd.clone(), - approval_policy: test.config.approval_policy.value(), - sandbox_policy: test.config.sandbox_policy.get().clone(), + approval_policy: test.config.permissions.approval_policy.value(), + sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), model: test.session_configured.model.clone(), effort: None, summary: test.config.model_reasoning_summary, @@ -271,8 +271,8 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu text_elements: Vec::new(), }], cwd: test.config.cwd.clone(), - approval_policy: test.config.approval_policy.value(), - sandbox_policy: test.config.sandbox_policy.get().clone(), + approval_policy: test.config.permissions.approval_policy.value(), + sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), model: test.session_configured.model.clone(), effort: None, summary: test.config.model_reasoning_summary, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 28bf7d71df..38f223769f 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -964,8 +964,9 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.sandbox_policy = Constrained::allow_any(SandboxPolicy::DangerFullAccess); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.sandbox_policy = + Constrained::allow_any(SandboxPolicy::DangerFullAccess); }) .build(&server) .await @@ -1015,7 +1016,8 @@ async fn handle_container_exec_user_approved_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await @@ -1080,7 +1082,8 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await @@ -1145,7 +1148,8 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await @@ -1210,7 +1214,8 @@ async fn handle_container_exec_user_denies_records_tool_decision() { .await; let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await @@ -1275,7 +1280,8 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await @@ -1341,7 +1347,8 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = + Constrained::allow_any(AskForApproval::UnlessTrusted); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index 0dbfbac157..d8455824b7 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -108,7 +108,7 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd let server = start_mock_server().await; let mut builder = test_codex().with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let test = builder.build(&server).await?; diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index c3936742e6..d272036067 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -55,7 +55,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> { .await; let mut builder = test_codex().with_config(move |config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let test = builder.build(&server).await?; @@ -96,7 +96,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { .await; let mut builder = test_codex().with_config(move |config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let test = builder.build(&server).await?; @@ -168,7 +168,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { .await; let mut builder = test_codex().with_config(move |config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let test = builder.build(&server).await?; @@ -230,7 +230,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { .await; let mut builder = test_codex().with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let initial = builder.build(&server).await?; let rollout_path = initial @@ -329,7 +329,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { .await; let mut builder = test_codex().with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); }); let initial = builder.build(&server).await?; let rollout_path = initial @@ -384,7 +384,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { assert_eq!(permissions_base.len(), 2); builder = builder.with_config(|config| { - config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); }); let resumed = builder.resume(&server, home, rollout_path.clone()).await?; resumed @@ -410,7 +410,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { assert!(!permissions_base.contains(permissions_resume.last().expect("new permissions"))); let mut fork_config = initial.config.clone(); - fork_config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + fork_config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); let forked = initial .thread_manager .fork_thread(usize::MAX, fork_config, rollout_path, false) @@ -465,8 +465,8 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { - config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); }); let test = builder.build(&server).await?; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 157afbd2ce..9397765c90 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -94,7 +94,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -141,7 +141,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -195,7 +195,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -255,7 +255,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -303,7 +303,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -337,7 +337,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -400,7 +400,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -434,7 +434,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -507,7 +507,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, @@ -541,7 +541,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: test.config.approval_policy.value(), + approval_policy: test.config.permissions.approval_policy.value(), sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 64a9b55f8b..97fe9b4161 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -733,8 +733,8 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a .await?; let default_cwd = config.cwd.clone(); - let default_approval_policy = config.approval_policy.value(); - let default_sandbox_policy = config.sandbox_policy.get(); + let default_approval_policy = config.permissions.approval_policy.value(); + let default_sandbox_policy = config.permissions.sandbox_policy.get(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort; let default_summary = config.model_reasoning_summary; @@ -841,8 +841,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .await?; let default_cwd = config.cwd.clone(); - let default_approval_policy = config.approval_policy.value(); - let default_sandbox_policy = config.sandbox_policy.get(); + let default_approval_policy = config.permissions.approval_policy.value(); + let default_sandbox_policy = config.permissions.sandbox_policy.get(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort; let default_summary = config.model_reasoning_summary; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index e20db08617..3af3a5952b 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -177,8 +177,8 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), - approval_policy: config.approval_policy.value(), - sandbox_policy: config.sandbox_policy.get().clone(), + approval_policy: config.permissions.approval_policy.value(), + sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: requested_model.to_string(), effort: None, summary: config.model_reasoning_summary, diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index d1746dbf8e..eb4e9742a4 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -22,8 +22,8 @@ fn resume_history( let turn_ctx = TurnContextItem { turn_id: None, cwd: config.cwd.clone(), - approval_policy: config.approval_policy.value(), - sandbox_policy: config.sandbox_policy.get().clone(), + approval_policy: config.permissions.approval_policy.value(), + sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: previous_model.to_string(), personality: None, collaboration_mode: None, diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index a828071ea5..5e62cd698f 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -416,6 +416,7 @@ async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { config + .permissions .sandbox_policy .set(SandboxPolicy::DangerFullAccess) .expect("set sandbox policy"); @@ -511,7 +512,8 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(|cfg| { - cfg.sandbox_policy + cfg.permissions + .sandbox_policy .set(SandboxPolicy::DangerFullAccess) .expect("set sandbox policy"); }); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f5ee8fcb47..31b2ff8615 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -332,8 +332,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any } let default_cwd = config.cwd.to_path_buf(); - let default_approval_policy = config.approval_policy.value(); - let default_sandbox_policy = config.sandbox_policy.get(); + let default_approval_policy = config.permissions.approval_policy.value(); + let default_sandbox_policy = config.permissions.sandbox_policy.get(); let default_effort = config.model_reasoning_effort; let default_summary = config.model_reasoning_summary; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 67d5e30a82..2f63688f73 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -628,7 +628,7 @@ impl App { fn apply_runtime_policy_overrides(&mut self, config: &mut Config) { if let Some(policy) = self.runtime_approval_policy_override.as_ref() - && let Err(err) = config.approval_policy.set(*policy) + && let Err(err) = config.permissions.approval_policy.set(*policy) { tracing::warn!(%err, "failed to carry forward approval policy override"); self.chat_widget.add_error_message(format!( @@ -636,7 +636,7 @@ impl App { )); } if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() - && let Err(err) = config.sandbox_policy.set(policy.clone()) + && let Err(err) = config.permissions.sandbox_policy.set(policy.clone()) { tracing::warn!(%err, "failed to carry forward sandbox policy override"); self.chat_widget.add_error_message(format!( @@ -1156,7 +1156,7 @@ impl App { let should_check = WindowsSandboxLevel::from_config(&app.config) != WindowsSandboxLevel::Disabled && matches!( - app.config.sandbox_policy.get(), + app.config.permissions.sandbox_policy.get(), codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } | codex_core::protocol::SandboxPolicy::ReadOnly { .. } ) @@ -1170,7 +1170,7 @@ impl App { let env_map: std::collections::HashMap = std::env::vars().collect(); let tx = app.app_event_tx.clone(); let logs_base_dir = app.config.codex_home.clone(); - let sandbox_policy = app.config.sandbox_policy.get().clone(); + let sandbox_policy = app.config.permissions.sandbox_policy.get().clone(); Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx); } } @@ -1913,8 +1913,9 @@ impl App { self.config.set_windows_sandbox_enabled(true); self.config.set_windows_elevated_sandbox_enabled(false); } - self.chat_widget - .set_windows_sandbox_mode(self.config.windows_sandbox_mode); + self.chat_widget.set_windows_sandbox_mode( + self.config.permissions.windows_sandbox_mode, + ); let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); if let Some((sample_paths, extra_count, failed_scan)) = @@ -2060,7 +2061,7 @@ impl App { } AppEvent::UpdateAskForApprovalPolicy(policy) => { self.runtime_approval_policy_override = Some(policy); - if let Err(err) = self.config.approval_policy.set(policy) { + if let Err(err) = self.config.permissions.approval_policy.set(policy) { tracing::warn!(%err, "failed to set approval policy on app config"); self.chat_widget .add_error_message(format!("Failed to set approval policy: {err}")); @@ -2077,7 +2078,7 @@ impl App { ); let policy_for_chat = policy.clone(); - if let Err(err) = self.config.sandbox_policy.set(policy) { + if let Err(err) = self.config.permissions.sandbox_policy.set(policy) { tracing::warn!(%err, "failed to set sandbox policy on app config"); self.chat_widget .add_error_message(format!("Failed to set sandbox policy: {err}")); @@ -2090,7 +2091,7 @@ impl App { return Ok(AppRunControl::Continue); } self.runtime_sandbox_policy_override = - Some(self.config.sandbox_policy.get().clone()); + Some(self.config.permissions.sandbox_policy.get().clone()); // If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan. #[cfg(target_os = "windows")] @@ -2111,7 +2112,7 @@ impl App { std::env::vars().collect(); let tx = self.app_event_tx.clone(); let logs_base_dir = self.config.codex_home.clone(); - let sandbox_policy = self.config.sandbox_policy.get().clone(); + let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); Self::spawn_world_writable_scan( cwd, env_map, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 46dcc19997..5dc8605f55 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3323,7 +3323,12 @@ impl ChatWidget { return; }; - if let Err(err) = self.config.approval_policy.can_set(&preset.approval) { + if let Err(err) = self + .config + .permissions + .approval_policy + .can_set(&preset.approval) + { self.add_error_message(err.to_string()); return; } @@ -3814,8 +3819,8 @@ impl ChatWidget { let op = Op::UserTurn { items, cwd: self.config.cwd.clone(), - approval_policy: self.config.approval_policy.value(), - sandbox_policy: self.config.sandbox_policy.get().clone(), + approval_policy: self.config.permissions.approval_policy.value(), + sandbox_policy: self.config.permissions.sandbox_policy.get().clone(), model: effective_mode.model().to_string(), effort: effective_mode.reasoning_effort(), summary: self.config.model_reasoning_summary, @@ -5272,8 +5277,8 @@ impl ChatWidget { /// Open a popup to choose the permissions mode (approval policy + sandbox policy). pub(crate) fn open_permissions_popup(&mut self) { let include_read_only = cfg!(target_os = "windows"); - let current_approval = self.config.approval_policy.value(); - let current_sandbox = self.config.sandbox_policy.get(); + let current_approval = self.config.permissions.approval_policy.value(); + let current_sandbox = self.config.permissions.sandbox_policy.get(); let mut items: Vec = Vec::new(); let presets: Vec = builtin_approval_presets(); @@ -5301,7 +5306,12 @@ impl ChatWidget { preset.label.to_string() }; let description = Some(preset.description.replace(" (Identical to Agent mode)", "")); - let disabled_reason = match self.config.approval_policy.can_set(&preset.approval) { + let disabled_reason = match self + .config + .permissions + .approval_policy + .can_set(&preset.approval) + { Ok(()) => None, Err(err) => Some(err.to_string()), }; @@ -5462,7 +5472,7 @@ impl ChatWidget { self.config.codex_home.as_path(), cwd.as_path(), &env_map, - self.config.sandbox_policy.get(), + self.config.permissions.sandbox_policy.get(), Some(self.config.codex_home.as_path()), ) { Ok(_) => None, @@ -5569,7 +5579,7 @@ impl ChatWidget { let mode_label = preset .as_ref() .map(|p| describe_policy(&p.sandbox)) - .unwrap_or_else(|| describe_policy(self.config.sandbox_policy.get())); + .unwrap_or_else(|| describe_policy(self.config.permissions.sandbox_policy.get())); let info_line = if failed_scan { Line::from(vec![ "We couldn't complete the world-writable scan, so protections cannot be verified. " @@ -5912,7 +5922,7 @@ impl ChatWidget { /// Set the approval policy in the widget's config copy. pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) { - if let Err(err) = self.config.approval_policy.set(policy) { + if let Err(err) = self.config.permissions.approval_policy.set(policy) { tracing::warn!(%err, "failed to set approval_policy on chat config"); } } @@ -5920,13 +5930,13 @@ impl ChatWidget { /// Set the sandbox policy in the widget's config copy. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { - self.config.sandbox_policy.set(policy)?; + self.config.permissions.sandbox_policy.set(policy)?; Ok(()) } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option) { - self.config.windows_sandbox_mode = mode; + self.config.permissions.windows_sandbox_mode = mode; #[cfg(target_os = "windows")] self.bottom_pane.set_windows_degraded_sandbox_active( codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index e3dc0af342..82b22790f0 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -4510,7 +4510,7 @@ async fn disabled_slash_command_while_task_running_snapshot() { async fn approvals_popup_shows_disabled_presets() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.config.approval_policy = + chat.config.permissions.approval_policy = Constrained::new(AskForApproval::OnRequest, |candidate| match candidate { AskForApproval::OnRequest => Ok(()), _ => Err(invalid_value( @@ -4546,7 +4546,7 @@ async fn approvals_popup_shows_disabled_presets() { async fn approvals_popup_navigation_skips_disabled() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await; - chat.config.approval_policy = + chat.config.permissions.approval_policy = Constrained::new(AskForApproval::OnRequest, |candidate| match candidate { AskForApproval::OnRequest => Ok(()), _ => Err(invalid_value(candidate.to_string(), "[on-request]")), @@ -4623,7 +4623,10 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { // Build a chat widget with manual channels to avoid spawning the agent. let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; // Ensure policy allows surfacing approvals explicitly (not strictly required for direct event). - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; // Inject an exec approval request to display the approval modal. let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd".into(), @@ -4678,7 +4681,10 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { #[tokio::test] async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd-noreason".into(), @@ -4720,7 +4726,10 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() -> anyhow::Result<()> { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; let script = "python - <<'PY'\nprint('hello')\nPY".to_string(); let command = vec!["bash".into(), "-lc".into(), script]; @@ -4760,7 +4769,10 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() #[tokio::test] async fn approval_modal_patch_snapshot() -> anyhow::Result<()> { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; // Build a small changeset and a reason/grant_root to exercise the prompt text. let mut changes = HashMap::new(); @@ -5529,7 +5541,10 @@ async fn apply_patch_full_flow_integration_like() { async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; // Ensure approval policy is untrusted (OnRequest) - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; // Simulate a patch approval request from backend let mut changes = HashMap::new(); @@ -5577,7 +5592,10 @@ async fn apply_patch_request_shows_diff_summary() -> anyhow::Result<()> { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; // Ensure we are in OnRequest so an approval is surfaced - chat.config.approval_policy.set(AskForApproval::OnRequest)?; + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest)?; // Simulate backend asking to apply a patch adding two lines to README.md let mut changes = HashMap::new(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 135a0b8827..a3f32d298d 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -33,6 +33,7 @@ pub(crate) fn new_debug_config_output( http_addr, socks_addr, config + .permissions .network .as_ref() .is_some_and(codex_core::config::NetworkProxySpec::socks_enabled), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 87a813f96b..df9ecdf761 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -290,7 +290,9 @@ pub async fn run_main( .await; set_default_client_residency_requirement(config.enforce_residency.value()); - if let Some(warning) = add_dir_warning_message(&cli.add_dir, config.sandbox_policy.get()) { + if let Some(warning) = + add_dir_warning_message(&cli.add_dir, config.permissions.sandbox_policy.get()) + { #[allow(clippy::print_stderr)] { eprintln!("Error adding directories: {warning}"); @@ -1004,8 +1006,8 @@ mod tests { TurnContextItem { turn_id: None, cwd, - approval_policy: config.approval_policy.value(), - sandbox_policy: config.sandbox_policy.get().clone(), + approval_policy: config.permissions.approval_policy.value(), + sandbox_policy: config.permissions.sandbox_policy.get().clone(), model, personality: None, collaboration_mode: None, @@ -1123,7 +1125,7 @@ trust_level = "untrusted" .build() .await?; assert_eq!( - trusted_config.approval_policy.value(), + trusted_config.permissions.approval_policy.value(), AskForApproval::OnRequest ); @@ -1137,7 +1139,7 @@ trust_level = "untrusted" .build() .await?; assert_eq!( - untrusted_config.approval_policy.value(), + untrusted_config.permissions.approval_policy.value(), AskForApproval::UnlessTrusted ); Ok(()) diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 224c96fa99..e6d5a27d29 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -168,10 +168,13 @@ impl StatusHistoryCell { ("workdir", config.cwd.display().to_string()), ("model", model_name.to_string()), ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.value().to_string()), + ( + "approval", + config.permissions.approval_policy.value().to_string(), + ), ( "sandbox", - summarize_sandbox_policy(config.sandbox_policy.get()), + summarize_sandbox_policy(config.permissions.sandbox_policy.get()), ), ]; if config.model_provider.wire_api == WireApi::Responses { @@ -191,7 +194,7 @@ impl StatusHistoryCell { .find(|(k, _)| *k == "approval") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "".to_string()); - let sandbox = match config.sandbox_policy.get() { + let sandbox = match config.permissions.sandbox_policy.get() { SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), SandboxPolicy::ReadOnly { .. } => "read-only".to_string(), SandboxPolicy::WorkspaceWrite { @@ -207,12 +210,13 @@ impl StatusHistoryCell { } } }; - let permissions = if config.approval_policy.value() == AskForApproval::OnRequest - && *config.sandbox_policy.get() == SandboxPolicy::new_workspace_write_policy() + let permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest + && *config.permissions.sandbox_policy.get() + == SandboxPolicy::new_workspace_write_policy() { "Default".to_string() - } else if config.approval_policy.value() == AskForApproval::Never - && *config.sandbox_policy.get() == SandboxPolicy::DangerFullAccess + } else if config.permissions.approval_policy.value() == AskForApproval::Never + && *config.permissions.sandbox_policy.get() == SandboxPolicy::DangerFullAccess { "Full Access".to_string() } else { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index c0e0384728..02838c651e 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -98,6 +98,7 @@ async fn status_snapshot_includes_reasoning_details() { config.model_provider_id = "openai".to_string(); config.model_reasoning_summary = ReasoningSummary::Detailed; config + .permissions .sandbox_policy .set(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), @@ -177,10 +178,12 @@ async fn status_permissions_non_default_workspace_write_is_custom() { config.model = Some("gpt-5.1-codex-max".to_string()); config.model_provider_id = "openai".to_string(); config + .permissions .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); config + .permissions .sandbox_policy .set(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), diff --git a/codex-rs/utils/sandbox-summary/src/config_summary.rs b/codex-rs/utils/sandbox-summary/src/config_summary.rs index 1eeabfb533..a83a65723b 100644 --- a/codex-rs/utils/sandbox-summary/src/config_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/config_summary.rs @@ -9,10 +9,13 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta ("workdir", config.cwd.display().to_string()), ("model", model.to_string()), ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.value().to_string()), + ( + "approval", + config.permissions.approval_policy.value().to_string(), + ), ( "sandbox", - summarize_sandbox_policy(config.sandbox_policy.get()), + summarize_sandbox_policy(config.permissions.sandbox_policy.get()), ), ]; if config.model_provider.wire_api == WireApi::Responses {