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