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