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