agent.rs

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