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