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