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