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