agent.rs

  1use collections::{HashMap, IndexMap};
  2use schemars::{JsonSchema, json_schema};
  3use serde::{Deserialize, Serialize};
  4use settings_macros::{MergeFrom, with_fallible_options};
  5use std::sync::Arc;
  6use std::{borrow::Cow, path::PathBuf};
  7
  8use crate::ExtendingVec;
  9
 10use crate::DockPosition;
 11
 12/// Where new threads should start by default.
 13#[derive(
 14    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
 15)]
 16#[serde(rename_all = "snake_case")]
 17pub enum NewThreadLocation {
 18    /// Start threads in the current project.
 19    #[default]
 20    LocalProject,
 21    /// Start threads in a new worktree.
 22    NewWorktree,
 23}
 24
 25#[with_fallible_options]
 26#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
 27pub struct AgentSettingsContent {
 28    /// Whether the Agent is enabled.
 29    ///
 30    /// Default: true
 31    pub enabled: Option<bool>,
 32    /// Whether to show the agent panel button in the status bar.
 33    ///
 34    /// Default: true
 35    pub button: Option<bool>,
 36    /// Where to dock the agent panel.
 37    ///
 38    /// Default: right
 39    pub dock: Option<DockPosition>,
 40    /// Default width in pixels when the agent panel is docked to the left or right.
 41    ///
 42    /// Default: 640
 43    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
 44    pub default_width: Option<f32>,
 45    /// Default height in pixels when the agent panel is docked to the bottom.
 46    ///
 47    /// Default: 320
 48    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
 49    pub default_height: Option<f32>,
 50    /// The default model to use when creating new chats and for other features when a specific model is not specified.
 51    pub default_model: Option<LanguageModelSelection>,
 52    /// Favorite models to show at the top of the model selector.
 53    #[serde(default)]
 54    pub favorite_models: Vec<LanguageModelSelection>,
 55    /// Model to use for the inline assistant. Defaults to default_model when not specified.
 56    pub inline_assistant_model: Option<LanguageModelSelection>,
 57    /// Model to use for the inline assistant when streaming tools are enabled.
 58    ///
 59    /// Default: true
 60    pub inline_assistant_use_streaming_tools: Option<bool>,
 61    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
 62    pub commit_message_model: Option<LanguageModelSelection>,
 63    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
 64    pub thread_summary_model: Option<LanguageModelSelection>,
 65    /// Additional models with which to generate alternatives when performing inline assists.
 66    pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
 67    /// The default profile to use in the Agent.
 68    ///
 69    /// Default: write
 70    pub default_profile: Option<Arc<str>>,
 71    /// Which view type to show by default in the agent panel.
 72    ///
 73    /// Default: "thread"
 74    pub default_view: Option<DefaultAgentView>,
 75    /// Where new threads should start by default.
 76    ///
 77    /// Default: "local_project"
 78    pub new_thread_location: Option<NewThreadLocation>,
 79    /// The available agent profiles.
 80    pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
 81    /// Where to show a popup notification when the agent is waiting for user input.
 82    ///
 83    /// Default: "primary_screen"
 84    pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
 85    /// Whether to play a sound when the agent has either completed its response, or needs user input.
 86    ///
 87    /// Default: false
 88    pub play_sound_when_agent_done: Option<bool>,
 89    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
 90    ///
 91    /// Default: true
 92    pub single_file_review: Option<bool>,
 93    /// Additional parameters for language model requests. When making a request
 94    /// to a model, parameters will be taken from the last entry in this list
 95    /// that matches the model's provider and name. In each entry, both provider
 96    /// and model are optional, so that you can specify parameters for either
 97    /// one.
 98    ///
 99    /// Default: []
100    #[serde(default)]
101    pub model_parameters: Vec<LanguageModelParameters>,
102    /// Whether to show thumb buttons for feedback in the agent panel.
103    ///
104    /// Default: true
105    pub enable_feedback: Option<bool>,
106    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
107    ///
108    /// Default: true
109    pub expand_edit_card: Option<bool>,
110    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
111    ///
112    /// Default: true
113    pub expand_terminal_card: Option<bool>,
114    /// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
115    /// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
116    ///
117    /// Default: true
118    pub cancel_generation_on_terminal_stop: Option<bool>,
119    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
120    ///
121    /// Default: false
122    pub use_modifier_to_send: Option<bool>,
123    /// Minimum number of lines of height the agent message editor should have.
124    ///
125    /// Default: 4
126    pub message_editor_min_lines: Option<usize>,
127    /// Whether to show turn statistics (elapsed time during generation, final turn duration).
128    ///
129    /// Default: false
130    pub show_turn_stats: Option<bool>,
131    /// Per-tool permission rules for granular control over which tool actions
132    /// require confirmation.
133    ///
134    /// The global `default` applies when no tool-specific rules match.
135    /// For external agent servers (e.g. Claude Agent) that define their own
136    /// permission modes, "deny" and "confirm" still take precedence — the
137    /// external agent's permission system is only used when Zed would allow
138    /// the action. Per-tool regex patterns (`always_allow`, `always_deny`,
139    /// `always_confirm`) match against the tool's text input (command, path,
140    /// URL, etc.).
141    pub tool_permissions: Option<ToolPermissionsContent>,
142}
143
144impl AgentSettingsContent {
145    pub fn set_dock(&mut self, dock: DockPosition) {
146        self.dock = Some(dock);
147    }
148
149    pub fn set_model(&mut self, language_model: LanguageModelSelection) {
150        self.default_model = Some(language_model)
151    }
152
153    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
154        self.inline_assistant_model = Some(LanguageModelSelection {
155            provider: provider.into(),
156            model,
157            enable_thinking: false,
158            effort: None,
159        });
160    }
161
162    pub fn set_profile(&mut self, profile_id: Arc<str>) {
163        self.default_profile = Some(profile_id);
164    }
165
166    pub fn set_new_thread_location(&mut self, value: NewThreadLocation) {
167        self.new_thread_location = Some(value);
168    }
169
170    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
171        if !self.favorite_models.contains(&model) {
172            self.favorite_models.push(model);
173        }
174    }
175
176    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
177        self.favorite_models.retain(|m| m != model);
178    }
179
180    pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
181        let tool_permissions = self.tool_permissions.get_or_insert_default();
182        let tool_rules = tool_permissions
183            .tools
184            .entry(Arc::from(tool_id))
185            .or_default();
186        tool_rules.default = Some(mode);
187    }
188
189    pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
190        let tool_permissions = self.tool_permissions.get_or_insert_default();
191        let tool_rules = tool_permissions
192            .tools
193            .entry(Arc::from(tool_name))
194            .or_default();
195        let always_allow = tool_rules.always_allow.get_or_insert_default();
196        if !always_allow.0.iter().any(|r| r.pattern == pattern) {
197            always_allow.0.push(ToolRegexRule {
198                pattern,
199                case_sensitive: None,
200            });
201        }
202    }
203
204    pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
205        let tool_permissions = self.tool_permissions.get_or_insert_default();
206        let tool_rules = tool_permissions
207            .tools
208            .entry(Arc::from(tool_name))
209            .or_default();
210        let always_deny = tool_rules.always_deny.get_or_insert_default();
211        if !always_deny.0.iter().any(|r| r.pattern == pattern) {
212            always_deny.0.push(ToolRegexRule {
213                pattern,
214                case_sensitive: None,
215            });
216        }
217    }
218}
219
220#[with_fallible_options]
221#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
222pub struct AgentProfileContent {
223    pub name: Arc<str>,
224    #[serde(default)]
225    pub tools: IndexMap<Arc<str>, bool>,
226    /// Whether all context servers are enabled by default.
227    pub enable_all_context_servers: Option<bool>,
228    #[serde(default)]
229    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
230    /// The default language model selected when using this profile.
231    pub default_model: Option<LanguageModelSelection>,
232}
233
234#[with_fallible_options]
235#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
236pub struct ContextServerPresetContent {
237    pub tools: IndexMap<Arc<str>, bool>,
238}
239
240#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
241#[serde(rename_all = "snake_case")]
242pub enum DefaultAgentView {
243    #[default]
244    Thread,
245    TextThread,
246}
247
248#[derive(
249    Copy,
250    Clone,
251    Default,
252    Debug,
253    Serialize,
254    Deserialize,
255    JsonSchema,
256    MergeFrom,
257    PartialEq,
258    strum::VariantArray,
259    strum::VariantNames,
260)]
261#[serde(rename_all = "snake_case")]
262pub enum NotifyWhenAgentWaiting {
263    #[default]
264    PrimaryScreen,
265    AllScreens,
266    Never,
267}
268
269#[with_fallible_options]
270#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
271pub struct LanguageModelSelection {
272    pub provider: LanguageModelProviderSetting,
273    pub model: String,
274    #[serde(default)]
275    pub enable_thinking: bool,
276    pub effort: Option<String>,
277}
278
279#[with_fallible_options]
280#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
281pub struct LanguageModelParameters {
282    pub provider: Option<LanguageModelProviderSetting>,
283    pub model: Option<String>,
284    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
285    pub temperature: Option<f32>,
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
289pub struct LanguageModelProviderSetting(pub String);
290
291impl JsonSchema for LanguageModelProviderSetting {
292    fn schema_name() -> Cow<'static, str> {
293        "LanguageModelProviderSetting".into()
294    }
295
296    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
297        // list the builtin providers as a subset so that we still auto complete them in the settings
298        json_schema!({
299            "anyOf": [
300                {
301                    "type": "string",
302                    "enum": [
303                        "amazon-bedrock",
304                        "anthropic",
305                        "copilot_chat",
306                        "deepseek",
307                        "google",
308                        "lmstudio",
309                        "mistral",
310                        "ollama",
311                        "openai",
312                        "openrouter",
313                        "vercel",
314                        "vercel_ai_gateway",
315                        "x_ai",
316                        "zed.dev"
317                    ]
318                },
319                {
320                    "type": "string",
321                }
322            ]
323        })
324    }
325}
326
327impl From<String> for LanguageModelProviderSetting {
328    fn from(provider: String) -> Self {
329        Self(provider)
330    }
331}
332
333impl From<&str> for LanguageModelProviderSetting {
334    fn from(provider: &str) -> Self {
335        Self(provider.to_string())
336    }
337}
338
339#[with_fallible_options]
340#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
341#[serde(transparent)]
342pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
343
344impl std::ops::Deref for AllAgentServersSettings {
345    type Target = HashMap<String, CustomAgentServerSettings>;
346
347    fn deref(&self) -> &Self::Target {
348        &self.0
349    }
350}
351
352impl std::ops::DerefMut for AllAgentServersSettings {
353    fn deref_mut(&mut self) -> &mut Self::Target {
354        &mut self.0
355    }
356}
357
358#[with_fallible_options]
359#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
360#[serde(tag = "type", rename_all = "snake_case")]
361pub enum CustomAgentServerSettings {
362    Custom {
363        #[serde(rename = "command")]
364        path: PathBuf,
365        #[serde(default, skip_serializing_if = "Vec::is_empty")]
366        args: Vec<String>,
367        /// Default: {}
368        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
369        env: HashMap<String, String>,
370        /// The default mode to use for this agent.
371        ///
372        /// Note: Not only all agents support modes.
373        ///
374        /// Default: None
375        default_mode: Option<String>,
376        /// The default model to use for this agent.
377        ///
378        /// This should be the model ID as reported by the agent.
379        ///
380        /// Default: None
381        default_model: Option<String>,
382        /// The favorite models for this agent.
383        ///
384        /// These are the model IDs as reported by the agent.
385        ///
386        /// Default: []
387        #[serde(default, skip_serializing_if = "Vec::is_empty")]
388        favorite_models: Vec<String>,
389        /// Default values for session config options.
390        ///
391        /// This is a map from config option ID to value ID.
392        ///
393        /// Default: {}
394        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
395        default_config_options: HashMap<String, String>,
396        /// Favorited values for session config options.
397        ///
398        /// This is a map from config option ID to a list of favorited value IDs.
399        ///
400        /// Default: {}
401        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
402        favorite_config_option_values: HashMap<String, Vec<String>>,
403    },
404    Extension {
405        /// Additional environment variables to pass to the agent.
406        ///
407        /// Default: {}
408        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
409        env: HashMap<String, String>,
410        /// The default mode to use for this agent.
411        ///
412        /// Note: Not only all agents support modes.
413        ///
414        /// Default: None
415        default_mode: Option<String>,
416        /// The default model to use for this agent.
417        ///
418        /// This should be the model ID as reported by the agent.
419        ///
420        /// Default: None
421        default_model: Option<String>,
422        /// The favorite models for this agent.
423        ///
424        /// These are the model IDs as reported by the agent.
425        ///
426        /// Default: []
427        #[serde(default, skip_serializing_if = "Vec::is_empty")]
428        favorite_models: Vec<String>,
429        /// Default values for session config options.
430        ///
431        /// This is a map from config option ID to value ID.
432        ///
433        /// Default: {}
434        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
435        default_config_options: HashMap<String, String>,
436        /// Favorited values for session config options.
437        ///
438        /// This is a map from config option ID to a list of favorited value IDs.
439        ///
440        /// Default: {}
441        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
442        favorite_config_option_values: HashMap<String, Vec<String>>,
443    },
444    Registry {
445        /// Additional environment variables to pass to the agent.
446        ///
447        /// Default: {}
448        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
449        env: HashMap<String, String>,
450        /// The default mode to use for this agent.
451        ///
452        /// Note: Not only all agents support modes.
453        ///
454        /// Default: None
455        default_mode: Option<String>,
456        /// The default model to use for this agent.
457        ///
458        /// This should be the model ID as reported by the agent.
459        ///
460        /// Default: None
461        default_model: Option<String>,
462        /// The favorite models for this agent.
463        ///
464        /// These are the model IDs as reported by the agent.
465        ///
466        /// Default: []
467        #[serde(default, skip_serializing_if = "Vec::is_empty")]
468        favorite_models: Vec<String>,
469        /// Default values for session config options.
470        ///
471        /// This is a map from config option ID to value ID.
472        ///
473        /// Default: {}
474        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
475        default_config_options: HashMap<String, String>,
476        /// Favorited values for session config options.
477        ///
478        /// This is a map from config option ID to a list of favorited value IDs.
479        ///
480        /// Default: {}
481        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
482        favorite_config_option_values: HashMap<String, Vec<String>>,
483    },
484}
485
486#[with_fallible_options]
487#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
488pub struct ToolPermissionsContent {
489    /// Global default permission when no tool-specific rules match.
490    /// Individual tools can override this with their own default.
491    /// Default: confirm
492    #[serde(alias = "default_mode")]
493    pub default: Option<ToolPermissionMode>,
494
495    /// Per-tool permission rules.
496    /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
497    /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
498    /// even tools without meaningful text input can have a `default` set.
499    #[serde(default)]
500    pub tools: HashMap<Arc<str>, ToolRulesContent>,
501}
502
503#[with_fallible_options]
504#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
505pub struct ToolRulesContent {
506    /// Default mode when no regex rules match.
507    /// When unset, inherits from the global `tool_permissions.default`.
508    #[serde(alias = "default_mode")]
509    pub default: Option<ToolPermissionMode>,
510
511    /// Regexes for inputs to auto-approve.
512    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
513    /// For `copy_path` and `move_path`, patterns are matched independently against each
514    /// path (source and destination).
515    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
516    /// removed by a higher-priority layer—only new patterns can be added.
517    /// Default: []
518    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
519
520    /// Regexes for inputs to auto-reject.
521    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
522    /// For `copy_path` and `move_path`, patterns are matched independently against each
523    /// path (source and destination).
524    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
525    /// removed by a higher-priority layer—only new patterns can be added.
526    /// Default: []
527    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
528
529    /// Regexes for inputs that must always prompt.
530    /// Takes precedence over always_allow but not always_deny.
531    /// For `copy_path` and `move_path`, patterns are matched independently against each
532    /// path (source and destination).
533    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
534    /// removed by a higher-priority layer—only new patterns can be added.
535    /// Default: []
536    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
537}
538
539#[with_fallible_options]
540#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
541pub struct ToolRegexRule {
542    /// The regex pattern to match.
543    #[serde(default)]
544    pub pattern: String,
545
546    /// Whether the regex is case-sensitive.
547    /// Default: false (case-insensitive)
548    pub case_sensitive: Option<bool>,
549}
550
551#[derive(
552    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
553)]
554#[serde(rename_all = "snake_case")]
555pub enum ToolPermissionMode {
556    /// Auto-approve without prompting.
557    Allow,
558    /// Auto-reject with an error.
559    Deny,
560    /// Always prompt for confirmation (default behavior).
561    #[default]
562    Confirm,
563}
564
565impl std::fmt::Display for ToolPermissionMode {
566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567        match self {
568            ToolPermissionMode::Allow => write!(f, "Allow"),
569            ToolPermissionMode::Deny => write!(f, "Deny"),
570            ToolPermissionMode::Confirm => write!(f, "Confirm"),
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn test_set_tool_default_permission_creates_structure() {
581        let mut settings = AgentSettingsContent::default();
582        assert!(settings.tool_permissions.is_none());
583
584        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
585
586        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
587        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
588        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
589    }
590
591    #[test]
592    fn test_set_tool_default_permission_updates_existing() {
593        let mut settings = AgentSettingsContent::default();
594
595        settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
596        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
597
598        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
599        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
600        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
601    }
602
603    #[test]
604    fn test_set_tool_default_permission_for_mcp_tool() {
605        let mut settings = AgentSettingsContent::default();
606
607        settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);
608
609        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
610        let mcp_rules = tool_permissions
611            .tools
612            .get("mcp:github:create_issue")
613            .unwrap();
614        assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
615    }
616
617    #[test]
618    fn test_add_tool_allow_pattern_creates_structure() {
619        let mut settings = AgentSettingsContent::default();
620        assert!(settings.tool_permissions.is_none());
621
622        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
623
624        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
625        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
626        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
627        assert_eq!(always_allow.0.len(), 1);
628        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
629    }
630
631    #[test]
632    fn test_add_tool_allow_pattern_appends_to_existing() {
633        let mut settings = AgentSettingsContent::default();
634
635        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
636        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
637
638        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
639        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
640        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
641        assert_eq!(always_allow.0.len(), 2);
642        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
643        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
644    }
645
646    #[test]
647    fn test_add_tool_allow_pattern_does_not_duplicate() {
648        let mut settings = AgentSettingsContent::default();
649
650        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
651        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
652        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
653
654        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
655        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
656        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
657        assert_eq!(
658            always_allow.0.len(),
659            1,
660            "Duplicate patterns should not be added"
661        );
662    }
663
664    #[test]
665    fn test_add_tool_allow_pattern_for_different_tools() {
666        let mut settings = AgentSettingsContent::default();
667
668        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
669        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
670
671        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
672
673        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
674        assert_eq!(
675            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
676            "^cargo\\s"
677        );
678
679        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
680        assert_eq!(
681            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
682            "^https?://github\\.com"
683        );
684    }
685
686    #[test]
687    fn test_add_tool_deny_pattern_creates_structure() {
688        let mut settings = AgentSettingsContent::default();
689        assert!(settings.tool_permissions.is_none());
690
691        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
692
693        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
694        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
695        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
696        assert_eq!(always_deny.0.len(), 1);
697        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
698    }
699
700    #[test]
701    fn test_add_tool_deny_pattern_appends_to_existing() {
702        let mut settings = AgentSettingsContent::default();
703
704        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
705        settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
706
707        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
708        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
709        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
710        assert_eq!(always_deny.0.len(), 2);
711        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
712        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
713    }
714
715    #[test]
716    fn test_add_tool_deny_pattern_does_not_duplicate() {
717        let mut settings = AgentSettingsContent::default();
718
719        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
720        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
721        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
722
723        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
724        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
725        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
726        assert_eq!(
727            always_deny.0.len(),
728            1,
729            "Duplicate patterns should not be added"
730        );
731    }
732
733    #[test]
734    fn test_add_tool_deny_and_allow_patterns_separate() {
735        let mut settings = AgentSettingsContent::default();
736
737        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
738        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
739
740        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
741        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
742
743        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
744        assert_eq!(always_allow.0.len(), 1);
745        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
746
747        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
748        assert_eq!(always_deny.0.len(), 1);
749        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
750    }
751}