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