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