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