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