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