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