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