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