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