agent.rs

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