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