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