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