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        env: Option<HashMap<String, String>>,
436        /// The default mode to use for this agent.
437        ///
438        /// Note: Not only all agents support modes.
439        ///
440        /// Default: None
441        default_mode: Option<String>,
442        /// The default model to use for this agent.
443        ///
444        /// This should be the model ID as reported by the agent.
445        ///
446        /// Default: None
447        default_model: Option<String>,
448        /// The favorite models for this agent.
449        ///
450        /// These are the model IDs as reported by the agent.
451        ///
452        /// Default: []
453        #[serde(default)]
454        favorite_models: Vec<String>,
455        /// Default values for session config options.
456        ///
457        /// This is a map from config option ID to value ID.
458        ///
459        /// Default: {}
460        #[serde(default)]
461        default_config_options: HashMap<String, String>,
462        /// Favorited values for session config options.
463        ///
464        /// This is a map from config option ID to a list of favorited value IDs.
465        ///
466        /// Default: {}
467        #[serde(default)]
468        favorite_config_option_values: HashMap<String, Vec<String>>,
469    },
470    Extension {
471        /// The default mode to use for this agent.
472        ///
473        /// Note: Not only all agents support modes.
474        ///
475        /// Default: None
476        default_mode: Option<String>,
477        /// The default model to use for this agent.
478        ///
479        /// This should be the model ID as reported by the agent.
480        ///
481        /// Default: None
482        default_model: Option<String>,
483        /// The favorite models for this agent.
484        ///
485        /// These are the model IDs as reported by the agent.
486        ///
487        /// Default: []
488        #[serde(default)]
489        favorite_models: Vec<String>,
490        /// Default values for session config options.
491        ///
492        /// This is a map from config option ID to value ID.
493        ///
494        /// Default: {}
495        #[serde(default)]
496        default_config_options: HashMap<String, String>,
497        /// Favorited values for session config options.
498        ///
499        /// This is a map from config option ID to a list of favorited value IDs.
500        ///
501        /// Default: {}
502        #[serde(default)]
503        favorite_config_option_values: HashMap<String, Vec<String>>,
504    },
505}
506
507#[with_fallible_options]
508#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
509pub struct ToolPermissionsContent {
510    /// Per-tool permission rules.
511    /// Keys: terminal, edit_file, delete_path, move_path, create_directory,
512    ///       save_file, fetch, web_search
513    #[serde(default)]
514    pub tools: HashMap<Arc<str>, ToolRulesContent>,
515}
516
517#[with_fallible_options]
518#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
519pub struct ToolRulesContent {
520    /// Default mode when no regex rules match.
521    /// Default: confirm
522    pub default_mode: Option<ToolPermissionMode>,
523
524    /// Regexes for inputs to auto-approve.
525    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
526    /// Default: []
527    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
528
529    /// Regexes for inputs to auto-reject.
530    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
531    /// Default: []
532    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
533
534    /// Regexes for inputs that must always prompt.
535    /// Takes precedence over always_allow but not always_deny.
536    /// Default: []
537    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
538}
539
540#[with_fallible_options]
541#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
542pub struct ToolRegexRule {
543    /// The regex pattern to match.
544    #[serde(default)]
545    pub pattern: String,
546
547    /// Whether the regex is case-sensitive.
548    /// Default: false (case-insensitive)
549    pub case_sensitive: Option<bool>,
550}
551
552#[derive(
553    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
554)]
555#[serde(rename_all = "snake_case")]
556pub enum ToolPermissionMode {
557    /// Auto-approve without prompting.
558    Allow,
559    /// Auto-reject with an error.
560    Deny,
561    /// Always prompt for confirmation (default behavior).
562    #[default]
563    Confirm,
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_set_tool_default_mode_creates_structure() {
572        let mut settings = AgentSettingsContent::default();
573        assert!(settings.tool_permissions.is_none());
574
575        settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
576
577        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
578        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
579        assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
580    }
581
582    #[test]
583    fn test_set_tool_default_mode_updates_existing() {
584        let mut settings = AgentSettingsContent::default();
585
586        settings.set_tool_default_mode("terminal", ToolPermissionMode::Confirm);
587        settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
588
589        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
590        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
591        assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
592    }
593
594    #[test]
595    fn test_set_tool_default_mode_for_mcp_tool() {
596        let mut settings = AgentSettingsContent::default();
597
598        settings.set_tool_default_mode("mcp:github:create_issue", ToolPermissionMode::Allow);
599
600        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
601        let mcp_rules = tool_permissions
602            .tools
603            .get("mcp:github:create_issue")
604            .unwrap();
605        assert_eq!(mcp_rules.default_mode, Some(ToolPermissionMode::Allow));
606    }
607
608    #[test]
609    fn test_add_tool_allow_pattern_creates_structure() {
610        let mut settings = AgentSettingsContent::default();
611        assert!(settings.tool_permissions.is_none());
612
613        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
614
615        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
616        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
617        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
618        assert_eq!(always_allow.0.len(), 1);
619        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
620    }
621
622    #[test]
623    fn test_add_tool_allow_pattern_appends_to_existing() {
624        let mut settings = AgentSettingsContent::default();
625
626        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
627        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
628
629        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
630        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
631        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
632        assert_eq!(always_allow.0.len(), 2);
633        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
634        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
635    }
636
637    #[test]
638    fn test_add_tool_allow_pattern_does_not_duplicate() {
639        let mut settings = AgentSettingsContent::default();
640
641        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
642        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
643        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
644
645        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
646        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
647        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
648        assert_eq!(
649            always_allow.0.len(),
650            1,
651            "Duplicate patterns should not be added"
652        );
653    }
654
655    #[test]
656    fn test_add_tool_allow_pattern_for_different_tools() {
657        let mut settings = AgentSettingsContent::default();
658
659        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
660        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
661
662        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
663
664        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
665        assert_eq!(
666            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
667            "^cargo\\s"
668        );
669
670        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
671        assert_eq!(
672            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
673            "^https?://github\\.com"
674        );
675    }
676
677    #[test]
678    fn test_add_tool_deny_pattern_creates_structure() {
679        let mut settings = AgentSettingsContent::default();
680        assert!(settings.tool_permissions.is_none());
681
682        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
683
684        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
685        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
686        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
687        assert_eq!(always_deny.0.len(), 1);
688        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
689    }
690
691    #[test]
692    fn test_add_tool_deny_pattern_appends_to_existing() {
693        let mut settings = AgentSettingsContent::default();
694
695        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
696        settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
697
698        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
699        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
700        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
701        assert_eq!(always_deny.0.len(), 2);
702        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
703        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
704    }
705
706    #[test]
707    fn test_add_tool_deny_pattern_does_not_duplicate() {
708        let mut settings = AgentSettingsContent::default();
709
710        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
711        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
712        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
713
714        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
715        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
716        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
717        assert_eq!(
718            always_deny.0.len(),
719            1,
720            "Duplicate patterns should not be added"
721        );
722    }
723
724    #[test]
725    fn test_add_tool_deny_and_allow_patterns_separate() {
726        let mut settings = AgentSettingsContent::default();
727
728        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
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
734        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
735        assert_eq!(always_allow.0.len(), 1);
736        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
737
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}