diff --git a/codex-rs/config/src/mcp_types.rs b/codex-rs/config/src/mcp_types.rs index ada6b91394..a91b063b89 100644 --- a/codex-rs/config/src/mcp_types.rs +++ b/codex-rs/config/src/mcp_types.rs @@ -30,6 +30,22 @@ pub enum AppToolApproval { Approve, } +impl AppToolApproval { + /// Requires approval whenever either policy could require it. + /// + /// `Auto` and `Writes` are incomparable: each can require approval for a + /// tool the other would approve. Their conservative intersection is `Prompt`. + pub fn restrict_to(self, requested: Self) -> Self { + match (self, requested) { + (Self::Prompt, _) | (_, Self::Prompt) => Self::Prompt, + (Self::Approve, mode) | (mode, Self::Approve) => mode, + (Self::Auto, Self::Auto) => Self::Auto, + (Self::Writes, Self::Writes) => Self::Writes, + (Self::Auto, Self::Writes) | (Self::Writes, Self::Auto) => Self::Prompt, + } + } +} + /// Human-readable reason a configured MCP server was disabled after requirements /// were applied. /// diff --git a/codex-rs/config/src/mcp_types_tests.rs b/codex-rs/config/src/mcp_types_tests.rs index e291ed392e..c6ff37e0fa 100644 --- a/codex-rs/config/src/mcp_types_tests.rs +++ b/codex-rs/config/src/mcp_types_tests.rs @@ -4,6 +4,32 @@ use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::Path; +#[test] +fn app_tool_approval_restrictions_never_weaken_either_policy() { + use AppToolApproval::Approve; + use AppToolApproval::Auto; + use AppToolApproval::Prompt; + use AppToolApproval::Writes; + + let modes = [Approve, Auto, Writes, Prompt]; + let expected = [ + [Approve, Auto, Writes, Prompt], + [Auto, Auto, Prompt, Prompt], + [Writes, Prompt, Writes, Prompt], + [Prompt, Prompt, Prompt, Prompt], + ]; + + for (parent_index, parent) in modes.into_iter().enumerate() { + for (requested_index, requested) in modes.into_iter().enumerate() { + assert_eq!( + parent.restrict_to(requested), + expected[parent_index][requested_index], + "parent: {parent:?}, requested: {requested:?}", + ); + } + } +} + #[test] fn deserialize_stdio_command_server_config() { let cfg: McpServerConfig = toml::from_str( diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 38a7217273..55dc633382 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -362,6 +362,18 @@ pub enum WebSearchMode { Live, } +impl WebSearchMode { + /// Restricts search to the access permitted by both modes. + pub fn restrict_to(self, requested: Self) -> Self { + match (self, requested) { + (Self::Disabled, _) | (_, Self::Disabled) => Self::Disabled, + (Self::Cached, _) | (_, Self::Cached) => Self::Cached, + (Self::Indexed, _) | (_, Self::Indexed) => Self::Indexed, + (Self::Live, Self::Live) => Self::Live, + } + } +} + /// A model-facing surface on which a tool can be exposed. #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] #[serde(rename_all = "snake_case")] @@ -768,6 +780,32 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn web_search_mode_restrictions_never_expand_either_mode() { + use WebSearchMode::Cached; + use WebSearchMode::Disabled; + use WebSearchMode::Indexed; + use WebSearchMode::Live; + + let modes = [Disabled, Cached, Indexed, Live]; + let expected = [ + [Disabled, Disabled, Disabled, Disabled], + [Disabled, Cached, Cached, Cached], + [Disabled, Cached, Indexed, Indexed], + [Disabled, Cached, Indexed, Live], + ]; + + for (parent_index, parent) in modes.into_iter().enumerate() { + for (requested_index, requested) in modes.into_iter().enumerate() { + assert_eq!( + parent.restrict_to(requested), + expected[parent_index][requested_index], + "parent: {parent:?}, requested: {requested:?}", + ); + } + } + } + #[test] fn apply_mask_can_clear_optional_fields() { let mode = CollaborationMode {