diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 603f34b0161..aaffa266f03 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 ca4e9999d0e..c19d58189fe 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 74eca4179d5..1dcf8b679e8 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 7f15d3226b8..75a2551f796 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(), @@ -1099,7 +1099,7 @@ impl Session { }); } maybe_push_unstable_features_warning(&config, &mut post_session_configured_events); - if config.approval_policy.value() == AskForApproval::OnFailure { + if config.permissions.approval_policy.value() == AskForApproval::OnFailure { post_session_configured_events.push(Event { id: "".to_owned(), msg: EventMsg::Warning(WarningEvent { @@ -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 28b619eed76..d5de27a40d5 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 ce7b8f07c25..bfa6daa5a86 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 4082c0f8e37..a2fd7298508 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 07a21a3fcd8..8e5f23450c3 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 02f740ebda2..363bec3c7e5 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 3b5e115ef86..7683339cebd 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 ed6912daae0..7c6799df920 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 a7a8bc7eec3..0cbdcef06e7 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 160866819a2..a6cad3a5ce8 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 28bf7d71dfc..38f223769fa 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 0dbfbac1573..d8455824b75 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 c3936742e64..d272036067b 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 157afbd2ceb..9397765c906 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 64a9b55f8b8..97fe9b41614 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 e20db086172..3af3a5952bb 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 d1746dbf8e0..eb4e9742a4e 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 a828071ea52..5e62cd698f7 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 f5ee8fcb47f..31b2ff8615e 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 67d5e30a82d..e30d490bbbe 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); } } @@ -1835,7 +1835,7 @@ impl App { None, )); - let policy = self.config.sandbox_policy.get().clone(); + let policy = self.config.permissions.sandbox_policy.get().clone(); let policy_cwd = self.config.cwd.clone(); let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = @@ -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 46dcc19997e..5dc8605f55f 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 e3dc0af3427..82b22790f06 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 135a0b88272..a3f32d298d4 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 87a813f96b9..df9ecdf761a 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 224c96fa996..e6d5a27d291 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 c0e03847283..02838c651e6 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 1eeabfb533b..a83a65723bd 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 {