mod agent_profile; use std::sync::Arc; use agent_client_protocol::ModelId;use collections::{HashSet, IndexMap};use gpui::{App, Pixels, px};use language_model::LanguageModel;use project::DisableAiSettings;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{ DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode,}; pub use crate::agent_profile::*; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = include_str!("prompts/summarize_thread_detailed_prompt.txt"); #[derive(Clone, Debug, RegisterSetting)]pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, pub agents_panel_dock: DockSide, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option<LanguageModelSelection>, pub inline_assistant_model: Option<LanguageModelSelection>, pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option<LanguageModelSelection>, pub thread_summary_model: Option<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>, pub favorite_models: Vec<LanguageModelSelection>, pub default_profile: AgentProfileId, pub default_view: DefaultAgentView, pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, pub play_sound_when_agent_done: bool, pub single_file_review: bool, pub model_parameters: Vec<LanguageModelParameters>, pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, pub cancel_generation_on_terminal_stop: bool, pub use_modifier_to_send: bool, pub message_editor_min_lines: usize, pub show_turn_stats: bool, pub tool_permissions: ToolPermissions,} impl AgentSettings { pub fn enabled(&self, cx: &App) -> bool { self.enabled && !DisableAiSettings::get_global(cx).disable_ai } pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> { let settings = Self::get_global(cx); for setting in settings.model_parameters.iter().rev() { if let Some(provider) = &setting.provider && provider.0 != model.provider_id().0 { continue; } if let Some(setting_model) = &setting.model && *setting_model != model.id().0 { continue; } return setting.temperature; } return None; } pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { self.inline_assistant_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_commit_message_model(&mut self, provider: String, model: String) { self.commit_message_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_thread_summary_model(&mut self, provider: String, model: String) { self.thread_summary_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_message_editor_max_lines(&self) -> usize { self.message_editor_min_lines * 2 } pub fn favorite_model_ids(&self) -> HashSet<ModelId> { self.favorite_models .iter() .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model))) .collect() }} #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]pub struct AgentProfileId(pub Arc<str>); impl AgentProfileId { pub fn as_str(&self) -> &str { &self.0 }} impl std::fmt::Display for AgentProfileId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }} impl Default for AgentProfileId { fn default() -> Self { Self("write".into()) }} #[derive(Clone, Debug, Default)]pub struct ToolPermissions { pub tools: collections::HashMap<Arc<str>, ToolRules>,} impl ToolPermissions { /// Returns all invalid regex patterns across all tools. pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> { self.tools .values() .flat_map(|rules| rules.invalid_patterns.iter()) .collect() } /// Returns true if any tool has invalid regex patterns. pub fn has_invalid_patterns(&self) -> bool { self.tools .values() .any(|rules| !rules.invalid_patterns.is_empty()) }} /// Represents a regex pattern that failed to compile.#[derive(Clone, Debug)]pub struct InvalidRegexPattern { /// The pattern string that failed to compile. pub pattern: String, /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm"). pub rule_type: String, /// The error message from the regex compiler. pub error: String,} #[derive(Clone, Debug)]pub struct ToolRules { pub default_mode: ToolPermissionMode, pub always_allow: Vec<CompiledRegex>, pub always_deny: Vec<CompiledRegex>, pub always_confirm: Vec<CompiledRegex>, /// Patterns that failed to compile. If non-empty, tool calls should be blocked. pub invalid_patterns: Vec<InvalidRegexPattern>,} impl Default for ToolRules { fn default() -> Self { Self { default_mode: ToolPermissionMode::Confirm, always_allow: Vec::new(), always_deny: Vec::new(), always_confirm: Vec::new(), invalid_patterns: Vec::new(), } }} #[derive(Clone)]pub struct CompiledRegex { pub pattern: String, pub case_sensitive: bool, pub regex: regex::Regex,} impl std::fmt::Debug for CompiledRegex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CompiledRegex") .field("pattern", &self.pattern) .field("case_sensitive", &self.case_sensitive) .finish() }} impl CompiledRegex { pub fn new(pattern: &str, case_sensitive: bool) -> Option<Self> { Self::try_new(pattern, case_sensitive).ok() } pub fn try_new(pattern: &str, case_sensitive: bool) -> Result<Self, regex::Error> { let regex = regex::RegexBuilder::new(pattern) .case_insensitive(!case_sensitive) .build()?; Ok(Self { pattern: pattern.to_string(), case_sensitive, regex, }) } pub fn is_match(&self, input: &str) -> bool { self.regex.is_match(input) }} impl Settings for AgentSettings { fn from_settings(content: &settings::SettingsContent) -> Self { let agent = content.agent.clone().unwrap(); Self { enabled: agent.enabled.unwrap(), button: agent.button.unwrap(), dock: agent.dock.unwrap(), agents_panel_dock: agent.agents_panel_dock.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), default_model: Some(agent.default_model.unwrap()), inline_assistant_model: agent.inline_assistant_model, inline_assistant_use_streaming_tools: agent .inline_assistant_use_streaming_tools .unwrap_or(true), commit_message_model: agent.commit_message_model, thread_summary_model: agent.thread_summary_model, inline_alternatives: agent.inline_alternatives.unwrap_or_default(), favorite_models: agent.favorite_models, default_profile: AgentProfileId(agent.default_profile.unwrap()), default_view: agent.default_view.unwrap(), profiles: agent .profiles .unwrap() .into_iter() .map(|(key, val)| (AgentProfileId(key), val.into())) .collect(), always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), single_file_review: agent.single_file_review.unwrap(), model_parameters: agent.model_parameters, enable_feedback: agent.enable_feedback.unwrap(), expand_edit_card: agent.expand_edit_card.unwrap(), expand_terminal_card: agent.expand_terminal_card.unwrap(), cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(), use_modifier_to_send: agent.use_modifier_to_send.unwrap(), message_editor_min_lines: agent.message_editor_min_lines.unwrap(), show_turn_stats: agent.show_turn_stats.unwrap(), tool_permissions: compile_tool_permissions(agent.tool_permissions), } }} fn compile_tool_permissions(content: Option<settings::ToolPermissionsContent>) -> ToolPermissions { let Some(content) = content else { return ToolPermissions::default(); }; let tools = content .tools .into_iter() .map(|(tool_name, rules_content)| { let mut invalid_patterns = Vec::new(); let (always_allow, allow_errors) = compile_regex_rules( rules_content.always_allow.map(|v| v.0).unwrap_or_default(), "always_allow", ); invalid_patterns.extend(allow_errors); let (always_deny, deny_errors) = compile_regex_rules( rules_content.always_deny.map(|v| v.0).unwrap_or_default(), "always_deny", ); invalid_patterns.extend(deny_errors); let (always_confirm, confirm_errors) = compile_regex_rules( rules_content .always_confirm .map(|v| v.0) .unwrap_or_default(), "always_confirm", ); invalid_patterns.extend(confirm_errors); // Log invalid patterns for debugging. Users will see an error when they // attempt to use a tool with invalid patterns in their settings. for invalid in &invalid_patterns { log::error!( "Invalid regex pattern in tool_permissions for '{}' tool ({}): '{}' - {}", tool_name, invalid.rule_type, invalid.pattern, invalid.error, ); } let rules = ToolRules { default_mode: rules_content.default_mode.unwrap_or_default(), always_allow, always_deny, always_confirm, invalid_patterns, }; (tool_name, rules) }) .collect(); ToolPermissions { tools }} fn compile_regex_rules( rules: Vec<settings::ToolRegexRule>, rule_type: &str,) -> (Vec<CompiledRegex>, Vec<InvalidRegexPattern>) { let mut compiled = Vec::new(); let mut errors = Vec::new(); for rule in rules { let case_sensitive = rule.case_sensitive.unwrap_or(false); match CompiledRegex::try_new(&rule.pattern, case_sensitive) { Ok(regex) => compiled.push(regex), Err(error) => { errors.push(InvalidRegexPattern { pattern: rule.pattern, rule_type: rule_type.to_string(), error: error.to_string(), }); } } } (compiled, errors)} #[cfg(test)]mod tests { use super::*; use serde_json::json; use settings::ToolPermissionsContent; #[test] fn test_compiled_regex_case_insensitive() { let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap(); assert!(regex.is_match("rm -rf /")); assert!(regex.is_match("RM -RF /")); assert!(regex.is_match("Rm -Rf /")); } #[test] fn test_compiled_regex_case_sensitive() { let regex = CompiledRegex::new("DROP\\s+TABLE", true).unwrap(); assert!(regex.is_match("DROP TABLE users")); assert!(!regex.is_match("drop table users")); } #[test] fn test_invalid_regex_returns_none() { let result = CompiledRegex::new("[invalid(regex", false); assert!(result.is_none()); } #[test] fn test_tool_permissions_parsing() { let json = json!({ "tools": { "terminal": { "default_mode": "allow", "always_deny": [ { "pattern": "rm\\s+-rf" } ], "always_allow": [ { "pattern": "^git\\s" } ] } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let terminal_rules = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal_rules.default_mode, ToolPermissionMode::Allow); assert_eq!(terminal_rules.always_deny.len(), 1); assert_eq!(terminal_rules.always_allow.len(), 1); assert!(terminal_rules.always_deny[0].is_match("rm -rf /")); assert!(terminal_rules.always_allow[0].is_match("git status")); } #[test] fn test_tool_rules_default_mode() { let json = json!({ "tools": { "edit_file": { "default_mode": "deny" } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let rules = permissions.tools.get("edit_file").unwrap(); assert_eq!(rules.default_mode, ToolPermissionMode::Deny); } #[test] fn test_tool_permissions_empty() { let permissions = compile_tool_permissions(None); assert!(permissions.tools.is_empty()); } #[test] fn test_tool_rules_default_returns_confirm() { let default_rules = ToolRules::default(); assert_eq!(default_rules.default_mode, ToolPermissionMode::Confirm); assert!(default_rules.always_allow.is_empty()); assert!(default_rules.always_deny.is_empty()); assert!(default_rules.always_confirm.is_empty()); } #[test] fn test_tool_permissions_with_multiple_tools() { let json = json!({ "tools": { "terminal": { "default_mode": "allow", "always_deny": [{ "pattern": "rm\\s+-rf" }] }, "edit_file": { "default_mode": "confirm", "always_deny": [{ "pattern": "\\.env$" }] }, "delete_path": { "default_mode": "deny" } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); assert_eq!(permissions.tools.len(), 3); let terminal = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal.default_mode, ToolPermissionMode::Allow); assert_eq!(terminal.always_deny.len(), 1); let edit_file = permissions.tools.get("edit_file").unwrap(); assert_eq!(edit_file.default_mode, ToolPermissionMode::Confirm); assert!(edit_file.always_deny[0].is_match("secrets.env")); let delete_path = permissions.tools.get("delete_path").unwrap(); assert_eq!(delete_path.default_mode, ToolPermissionMode::Deny); } #[test] fn test_tool_permissions_with_all_rule_types() { let json = json!({ "tools": { "terminal": { "always_deny": [{ "pattern": "rm\\s+-rf" }], "always_confirm": [{ "pattern": "sudo\\s" }], "always_allow": [{ "pattern": "^git\\s+status" }] } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let terminal = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal.always_deny.len(), 1); assert_eq!(terminal.always_confirm.len(), 1); assert_eq!(terminal.always_allow.len(), 1); assert!(terminal.always_deny[0].is_match("rm -rf /")); assert!(terminal.always_confirm[0].is_match("sudo apt install")); assert!(terminal.always_allow[0].is_match("git status")); } #[test] fn test_invalid_regex_is_tracked_and_valid_ones_still_compile() { let json = json!({ "tools": {Lines 1–500 of 864. Load more
mod agent_profile; use std::sync::Arc; use agent_client_protocol::ModelId; use collections::{HashSet, IndexMap}; use gpui::{App, Pixels, px}; use language_model::LanguageModel; use project::DisableAiSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode, }; pub use crate::agent_profile::*; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = include_str!("prompts/summarize_thread_detailed_prompt.txt"); #[derive(Clone, Debug, RegisterSetting)] pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, pub agents_panel_dock: DockSide, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option<LanguageModelSelection>, pub inline_assistant_model: Option<LanguageModelSelection>, pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option<LanguageModelSelection>, pub thread_summary_model: Option<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>, pub favorite_models: Vec<LanguageModelSelection>, pub default_profile: AgentProfileId, pub default_view: DefaultAgentView, pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, pub play_sound_when_agent_done: bool, pub single_file_review: bool, pub model_parameters: Vec<LanguageModelParameters>, pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, pub cancel_generation_on_terminal_stop: bool, pub use_modifier_to_send: bool, pub message_editor_min_lines: usize, pub show_turn_stats: bool, pub tool_permissions: ToolPermissions, } impl AgentSettings { pub fn enabled(&self, cx: &App) -> bool { self.enabled && !DisableAiSettings::get_global(cx).disable_ai } pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> { let settings = Self::get_global(cx); for setting in settings.model_parameters.iter().rev() { if let Some(provider) = &setting.provider && provider.0 != model.provider_id().0 { continue; } if let Some(setting_model) = &setting.model && *setting_model != model.id().0 { continue; } return setting.temperature; } return None; } pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { self.inline_assistant_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_commit_message_model(&mut self, provider: String, model: String) { self.commit_message_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_thread_summary_model(&mut self, provider: String, model: String) { self.thread_summary_model = Some(LanguageModelSelection { provider: provider.into(), model, }); } pub fn set_message_editor_max_lines(&self) -> usize { self.message_editor_min_lines * 2 } pub fn favorite_model_ids(&self) -> HashSet<ModelId> { self.favorite_models .iter() .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model))) .collect() } } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct AgentProfileId(pub Arc<str>); impl AgentProfileId { pub fn as_str(&self) -> &str { &self.0 } } impl std::fmt::Display for AgentProfileId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Default for AgentProfileId { fn default() -> Self { Self("write".into()) } } #[derive(Clone, Debug, Default)] pub struct ToolPermissions { pub tools: collections::HashMap<Arc<str>, ToolRules>, } impl ToolPermissions { /// Returns all invalid regex patterns across all tools. pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> { self.tools .values() .flat_map(|rules| rules.invalid_patterns.iter()) .collect() } /// Returns true if any tool has invalid regex patterns. pub fn has_invalid_patterns(&self) -> bool { self.tools .values() .any(|rules| !rules.invalid_patterns.is_empty()) } } /// Represents a regex pattern that failed to compile. #[derive(Clone, Debug)] pub struct InvalidRegexPattern { /// The pattern string that failed to compile. pub pattern: String, /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm"). pub rule_type: String, /// The error message from the regex compiler. pub error: String, } #[derive(Clone, Debug)] pub struct ToolRules { pub default_mode: ToolPermissionMode, pub always_allow: Vec<CompiledRegex>, pub always_deny: Vec<CompiledRegex>, pub always_confirm: Vec<CompiledRegex>, /// Patterns that failed to compile. If non-empty, tool calls should be blocked. pub invalid_patterns: Vec<InvalidRegexPattern>, } impl Default for ToolRules { fn default() -> Self { Self { default_mode: ToolPermissionMode::Confirm, always_allow: Vec::new(), always_deny: Vec::new(), always_confirm: Vec::new(), invalid_patterns: Vec::new(), } } } #[derive(Clone)] pub struct CompiledRegex { pub pattern: String, pub case_sensitive: bool, pub regex: regex::Regex, } impl std::fmt::Debug for CompiledRegex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CompiledRegex") .field("pattern", &self.pattern) .field("case_sensitive", &self.case_sensitive) .finish() } } impl CompiledRegex { pub fn new(pattern: &str, case_sensitive: bool) -> Option<Self> { Self::try_new(pattern, case_sensitive).ok() } pub fn try_new(pattern: &str, case_sensitive: bool) -> Result<Self, regex::Error> { let regex = regex::RegexBuilder::new(pattern) .case_insensitive(!case_sensitive) .build()?; Ok(Self { pattern: pattern.to_string(), case_sensitive, regex, }) } pub fn is_match(&self, input: &str) -> bool { self.regex.is_match(input) } } impl Settings for AgentSettings { fn from_settings(content: &settings::SettingsContent) -> Self { let agent = content.agent.clone().unwrap(); Self { enabled: agent.enabled.unwrap(), button: agent.button.unwrap(), dock: agent.dock.unwrap(), agents_panel_dock: agent.agents_panel_dock.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), default_model: Some(agent.default_model.unwrap()), inline_assistant_model: agent.inline_assistant_model, inline_assistant_use_streaming_tools: agent .inline_assistant_use_streaming_tools .unwrap_or(true), commit_message_model: agent.commit_message_model, thread_summary_model: agent.thread_summary_model, inline_alternatives: agent.inline_alternatives.unwrap_or_default(), favorite_models: agent.favorite_models, default_profile: AgentProfileId(agent.default_profile.unwrap()), default_view: agent.default_view.unwrap(), profiles: agent .profiles .unwrap() .into_iter() .map(|(key, val)| (AgentProfileId(key), val.into())) .collect(), always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), single_file_review: agent.single_file_review.unwrap(), model_parameters: agent.model_parameters, enable_feedback: agent.enable_feedback.unwrap(), expand_edit_card: agent.expand_edit_card.unwrap(), expand_terminal_card: agent.expand_terminal_card.unwrap(), cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(), use_modifier_to_send: agent.use_modifier_to_send.unwrap(), message_editor_min_lines: agent.message_editor_min_lines.unwrap(), show_turn_stats: agent.show_turn_stats.unwrap(), tool_permissions: compile_tool_permissions(agent.tool_permissions), } } } fn compile_tool_permissions(content: Option<settings::ToolPermissionsContent>) -> ToolPermissions { let Some(content) = content else { return ToolPermissions::default(); }; let tools = content .tools .into_iter() .map(|(tool_name, rules_content)| { let mut invalid_patterns = Vec::new(); let (always_allow, allow_errors) = compile_regex_rules( rules_content.always_allow.map(|v| v.0).unwrap_or_default(), "always_allow", ); invalid_patterns.extend(allow_errors); let (always_deny, deny_errors) = compile_regex_rules( rules_content.always_deny.map(|v| v.0).unwrap_or_default(), "always_deny", ); invalid_patterns.extend(deny_errors); let (always_confirm, confirm_errors) = compile_regex_rules( rules_content .always_confirm .map(|v| v.0) .unwrap_or_default(), "always_confirm", ); invalid_patterns.extend(confirm_errors); // Log invalid patterns for debugging. Users will see an error when they // attempt to use a tool with invalid patterns in their settings. for invalid in &invalid_patterns { log::error!( "Invalid regex pattern in tool_permissions for '{}' tool ({}): '{}' - {}", tool_name, invalid.rule_type, invalid.pattern, invalid.error, ); } let rules = ToolRules { default_mode: rules_content.default_mode.unwrap_or_default(), always_allow, always_deny, always_confirm, invalid_patterns, }; (tool_name, rules) }) .collect(); ToolPermissions { tools } } fn compile_regex_rules( rules: Vec<settings::ToolRegexRule>, rule_type: &str, ) -> (Vec<CompiledRegex>, Vec<InvalidRegexPattern>) { let mut compiled = Vec::new(); let mut errors = Vec::new(); for rule in rules { let case_sensitive = rule.case_sensitive.unwrap_or(false); match CompiledRegex::try_new(&rule.pattern, case_sensitive) { Ok(regex) => compiled.push(regex), Err(error) => { errors.push(InvalidRegexPattern { pattern: rule.pattern, rule_type: rule_type.to_string(), error: error.to_string(), }); } } } (compiled, errors) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use settings::ToolPermissionsContent; #[test] fn test_compiled_regex_case_insensitive() { let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap(); assert!(regex.is_match("rm -rf /")); assert!(regex.is_match("RM -RF /")); assert!(regex.is_match("Rm -Rf /")); } #[test] fn test_compiled_regex_case_sensitive() { let regex = CompiledRegex::new("DROP\\s+TABLE", true).unwrap(); assert!(regex.is_match("DROP TABLE users")); assert!(!regex.is_match("drop table users")); } #[test] fn test_invalid_regex_returns_none() { let result = CompiledRegex::new("[invalid(regex", false); assert!(result.is_none()); } #[test] fn test_tool_permissions_parsing() { let json = json!({ "tools": { "terminal": { "default_mode": "allow", "always_deny": [ { "pattern": "rm\\s+-rf" } ], "always_allow": [ { "pattern": "^git\\s" } ] } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let terminal_rules = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal_rules.default_mode, ToolPermissionMode::Allow); assert_eq!(terminal_rules.always_deny.len(), 1); assert_eq!(terminal_rules.always_allow.len(), 1); assert!(terminal_rules.always_deny[0].is_match("rm -rf /")); assert!(terminal_rules.always_allow[0].is_match("git status")); } #[test] fn test_tool_rules_default_mode() { let json = json!({ "tools": { "edit_file": { "default_mode": "deny" } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let rules = permissions.tools.get("edit_file").unwrap(); assert_eq!(rules.default_mode, ToolPermissionMode::Deny); } #[test] fn test_tool_permissions_empty() { let permissions = compile_tool_permissions(None); assert!(permissions.tools.is_empty()); } #[test] fn test_tool_rules_default_returns_confirm() { let default_rules = ToolRules::default(); assert_eq!(default_rules.default_mode, ToolPermissionMode::Confirm); assert!(default_rules.always_allow.is_empty()); assert!(default_rules.always_deny.is_empty()); assert!(default_rules.always_confirm.is_empty()); } #[test] fn test_tool_permissions_with_multiple_tools() { let json = json!({ "tools": { "terminal": { "default_mode": "allow", "always_deny": [{ "pattern": "rm\\s+-rf" }] }, "edit_file": { "default_mode": "confirm", "always_deny": [{ "pattern": "\\.env$" }] }, "delete_path": { "default_mode": "deny" } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); assert_eq!(permissions.tools.len(), 3); let terminal = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal.default_mode, ToolPermissionMode::Allow); assert_eq!(terminal.always_deny.len(), 1); let edit_file = permissions.tools.get("edit_file").unwrap(); assert_eq!(edit_file.default_mode, ToolPermissionMode::Confirm); assert!(edit_file.always_deny[0].is_match("secrets.env")); let delete_path = permissions.tools.get("delete_path").unwrap(); assert_eq!(delete_path.default_mode, ToolPermissionMode::Deny); } #[test] fn test_tool_permissions_with_all_rule_types() { let json = json!({ "tools": { "terminal": { "always_deny": [{ "pattern": "rm\\s+-rf" }], "always_confirm": [{ "pattern": "sudo\\s" }], "always_allow": [{ "pattern": "^git\\s+status" }] } } }); let content: ToolPermissionsContent = serde_json::from_value(json).unwrap(); let permissions = compile_tool_permissions(Some(content)); let terminal = permissions.tools.get("terminal").unwrap(); assert_eq!(terminal.always_deny.len(), 1); assert_eq!(terminal.always_confirm.len(), 1); assert_eq!(terminal.always_allow.len(), 1); assert!(terminal.always_deny[0].is_match("rm -rf /")); assert!(terminal.always_confirm[0].is_match("sudo apt install")); assert!(terminal.always_allow[0].is_match("git status")); } #[test] fn test_invalid_regex_is_tracked_and_valid_ones_still_compile() { let json = json!({ "tools": {