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