diff --git a/codex-rs/common/src/approval_presets.rs b/codex-rs/common/src/approval_presets.rs new file mode 100644 index 00000000..6c3bf395 --- /dev/null +++ b/codex-rs/common/src/approval_presets.rs @@ -0,0 +1,46 @@ +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; + +/// A simple preset pairing an approval policy with a sandbox policy. +#[derive(Debug, Clone)] +pub struct ApprovalPreset { + /// Stable identifier for the preset. + pub id: &'static str, + /// Display label shown in UIs. + pub label: &'static str, + /// Short human description shown next to the label in UIs. + pub description: &'static str, + /// Approval policy to apply. + pub approval: AskForApproval, + /// Sandbox policy to apply. + pub sandbox: SandboxPolicy, +} + +/// Built-in list of approval presets that pair approval and sandbox policy. +/// +/// Keep this UI-agnostic so it can be reused by both TUI and MCP server. +pub fn builtin_approval_presets() -> Vec { + vec![ + ApprovalPreset { + id: "read-only", + label: "Read Only", + description: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network.", + approval: AskForApproval::OnRequest, + sandbox: SandboxPolicy::ReadOnly, + }, + ApprovalPreset { + id: "auto", + label: "Auto", + description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network.", + approval: AskForApproval::OnRequest, + sandbox: SandboxPolicy::new_workspace_write_policy(), + }, + ApprovalPreset { + id: "full-access", + label: "Full Access", + description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution.", + approval: AskForApproval::Never, + sandbox: SandboxPolicy::DangerFullAccess, + }, + ] +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index dc684c21..292503f7 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -31,3 +31,6 @@ pub use config_summary::create_config_summary_entries; pub mod fuzzy_match; // Shared model presets used by TUI and MCP server pub mod model_presets; +// Shared approval presets (AskForApproval + Sandbox) used by TUI and MCP server +// Not to be confused with AskForApproval, which we should probably rename to EscalationPolicy. +pub mod approval_presets; diff --git a/codex-rs/config.md b/codex-rs/config.md index 8c2a7831..1af963a1 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -300,6 +300,16 @@ This is reasonable to use if Codex is running in an environment that provides it Though using this option may also be necessary if you try to use Codex in environments where its native sandboxing mechanisms are unsupported, such as older Linux kernels or on Windows. +## Approval presets + +Codex provides three main Approval Presets: + +- Read Only: Codex can read files and answer questions; edits, running commands, and network access require approval. +- Auto: Codex can read files, make edits, and run commands in the workspace without approval; asks for approval outside the workspace or for network access. +- Full Access: Full disk and network access without prompts; extremely risky. + +You can further customize how Codex runs at the command line using the `--ask-for-approval` and `--sandbox` options. + ## mcp_servers Defines the list of MCP servers that Codex can consult for tool use. Currently, only servers that are launched by executing a program that communicate over stdio are supported. For servers that use the SSE transport, consider an adapter like [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy). diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 78a6a778..9e0e31e1 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -33,18 +33,19 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-core = { path = "../core" } -codex-protocol = { path = "../protocol" } codex-file-search = { path = "../file-search" } codex-login = { path = "../login" } codex-ollama = { path = "../ollama" } +codex-protocol = { path = "../protocol" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } diffy = "0.4.2" image = { version = "^0.25.6", default-features = false, features = ["jpeg"] } lazy_static = "1" -once_cell = "1" mcp-types = { path = "../mcp-types" } +once_cell = "1" path-clean = "1.0.1" +rand = "0.9" ratatui = { version = "0.29.0", features = [ "scrolling-regions", "unstable-rendered-line-info", @@ -75,7 +76,6 @@ tui-markdown = "0.3.3" unicode-segmentation = "1.12.0" unicode-width = "0.1" uuid = "1" -rand = "0.9" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dd863c06..c7a16936 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -387,6 +387,11 @@ impl App<'_> { widget.open_model_popup(); } } + SlashCommand::Approvals => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.open_approvals_popup(); + } + } SlashCommand::Quit => { break; } @@ -514,6 +519,16 @@ impl App<'_> { widget.set_model(model); } } + AppEvent::UpdateAskForApprovalPolicy(policy) => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.set_approval_policy(policy); + } + } + AppEvent::UpdateSandboxPolicy(policy) => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.set_sandbox_policy(policy); + } + } } } terminal.clear()?; diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 1780dbc7..fc8b510f 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -6,6 +6,8 @@ use std::time::Duration; use crate::app::ChatWidgetArgs; use crate::slash_command::SlashCommand; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; use codex_core::protocol_config_types::ReasoningEffort; #[allow(clippy::large_enum_variant)] @@ -70,4 +72,10 @@ pub(crate) enum AppEvent { /// Update the current model slug in the running app and widget. UpdateModel(String), + + /// Update the current approval policy in the running app and widget. + UpdateAskForApprovalPolicy(AskForApproval), + + /// Update the current sandbox policy in the running app and widget. + UpdateSandboxPolicy(SandboxPolicy), } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 360a9f8e..0c72b621 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -60,9 +60,13 @@ mod agent; use self::agent::spawn_agent; use crate::streaming::controller::AppEventHistorySink; use crate::streaming::controller::StreamController; +use codex_common::approval_presets::ApprovalPreset; +use codex_common::approval_presets::builtin_approval_presets; use codex_common::model_presets::ModelPreset; use codex_common::model_presets::builtin_model_presets; use codex_core::ConversationManager; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig; use codex_file_search::FileMatch; use uuid::Uuid; @@ -733,6 +737,57 @@ impl ChatWidget<'_> { ); } + /// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy). + pub(crate) fn open_approvals_popup(&mut self) { + let current_approval = self.config.approval_policy; + let current_sandbox = self.config.sandbox_policy.clone(); + let mut items: Vec = Vec::new(); + let presets: Vec = builtin_approval_presets(); + for preset in presets.into_iter() { + let is_current = + current_approval == preset.approval && current_sandbox == preset.sandbox; + let approval = preset.approval; + let sandbox = preset.sandbox.clone(); + let name = preset.label.to_string(); + let description = Some(preset.description.to_string()); + let actions: Vec = vec![Box::new(move |tx| { + tx.send(AppEvent::CodexOp(Op::OverrideTurnContext { + cwd: None, + approval_policy: Some(approval), + sandbox_policy: Some(sandbox.clone()), + model: None, + effort: None, + summary: None, + })); + tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); + tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone())); + })]; + items.push(SelectionItem { + name, + description, + is_current, + actions, + }); + } + + self.bottom_pane.show_selection_view( + "Select Approvals Mode".to_string(), + None, + Some("Press Enter to confirm or Esc to go back".to_string()), + items, + ); + } + + /// Set the approval policy in the widget's config copy. + pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) { + self.config.approval_policy = policy; + } + + /// Set the sandbox policy in the widget's config copy. + pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) { + self.config.sandbox_policy = policy; + } + /// Set the reasoning effort in the widget's config copy. pub(crate) fn set_reasoning_effort(&mut self, effort: ReasoningEffortConfig) { self.config.model_reasoning_effort = effort; diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index d572895a..13ce2ff0 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -13,6 +13,7 @@ pub enum SlashCommand { // DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so // more frequently used commands should be listed first. Model, + Approvals, New, Init, Compact, @@ -38,6 +39,7 @@ impl SlashCommand { SlashCommand::Mention => "mention a file", SlashCommand::Status => "show current session configuration and token usage", SlashCommand::Model => "choose a model preset (model + reasoning effort)", + SlashCommand::Approvals => "choose what Codex can do without approval", SlashCommand::Mcp => "list configured MCP tools", SlashCommand::Logout => "log out of Codex", #[cfg(debug_assertions)]