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