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 /// When to play a sound when the agent has either completed its response, or needs user input.
163 ///
164 /// Default: never
165 pub play_sound_when_agent_done: Option<PlaySoundWhenAgentDone>,
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 /// Whether to show the merge conflict indicator in the status bar
213 /// that offers to resolve conflicts using the agent.
214 ///
215 /// Default: true
216 pub show_merge_conflict_indicator: Option<bool>,
217 /// Per-tool permission rules for granular control over which tool actions
218 /// require confirmation.
219 ///
220 /// The global `default` applies when no tool-specific rules match.
221 /// For external agent servers (e.g. Claude Agent) that define their own
222 /// permission modes, "deny" and "confirm" still take precedence — the
223 /// external agent's permission system is only used when Zed would allow
224 /// the action. Per-tool regex patterns (`always_allow`, `always_deny`,
225 /// `always_confirm`) match against the tool's text input (command, path,
226 /// URL, etc.).
227 pub tool_permissions: Option<ToolPermissionsContent>,
228}
229
230impl AgentSettingsContent {
231 pub fn set_dock(&mut self, dock: DockPosition) {
232 self.dock = Some(dock);
233 }
234
235 pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) {
236 self.sidebar_side = Some(position);
237 }
238
239 pub fn set_flexible_size(&mut self, flexible: bool) {
240 self.flexible = Some(flexible);
241 }
242
243 pub fn set_model(&mut self, language_model: LanguageModelSelection) {
244 self.default_model = Some(language_model)
245 }
246
247 pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
248 self.inline_assistant_model = Some(LanguageModelSelection {
249 provider: provider.into(),
250 model,
251 enable_thinking: false,
252 effort: None,
253 });
254 }
255
256 pub fn set_profile(&mut self, profile_id: Arc<str>) {
257 self.default_profile = Some(profile_id);
258 }
259
260 pub fn set_new_thread_location(&mut self, value: NewThreadLocation) {
261 self.new_thread_location = Some(value);
262 }
263
264 pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
265 if !self.favorite_models.contains(&model) {
266 self.favorite_models.push(model);
267 }
268 }
269
270 pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
271 self.favorite_models.retain(|m| m != model);
272 }
273
274 pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
275 let tool_permissions = self.tool_permissions.get_or_insert_default();
276 let tool_rules = tool_permissions
277 .tools
278 .entry(Arc::from(tool_id))
279 .or_default();
280 tool_rules.default = Some(mode);
281 }
282
283 pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
284 let tool_permissions = self.tool_permissions.get_or_insert_default();
285 let tool_rules = tool_permissions
286 .tools
287 .entry(Arc::from(tool_name))
288 .or_default();
289 let always_allow = tool_rules.always_allow.get_or_insert_default();
290 if !always_allow.0.iter().any(|r| r.pattern == pattern) {
291 always_allow.0.push(ToolRegexRule {
292 pattern,
293 case_sensitive: None,
294 });
295 }
296 }
297
298 pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
299 let tool_permissions = self.tool_permissions.get_or_insert_default();
300 let tool_rules = tool_permissions
301 .tools
302 .entry(Arc::from(tool_name))
303 .or_default();
304 let always_deny = tool_rules.always_deny.get_or_insert_default();
305 if !always_deny.0.iter().any(|r| r.pattern == pattern) {
306 always_deny.0.push(ToolRegexRule {
307 pattern,
308 case_sensitive: None,
309 });
310 }
311 }
312}
313
314#[with_fallible_options]
315#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
316pub struct AgentProfileContent {
317 pub name: Arc<str>,
318 #[serde(default)]
319 pub tools: IndexMap<Arc<str>, bool>,
320 /// Whether all context servers are enabled by default.
321 pub enable_all_context_servers: Option<bool>,
322 #[serde(default)]
323 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
324 /// The default language model selected when using this profile.
325 pub default_model: Option<LanguageModelSelection>,
326}
327
328#[with_fallible_options]
329#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
330pub struct ContextServerPresetContent {
331 pub tools: IndexMap<Arc<str>, bool>,
332}
333
334#[derive(
335 Copy,
336 Clone,
337 Default,
338 Debug,
339 Serialize,
340 Deserialize,
341 JsonSchema,
342 MergeFrom,
343 PartialEq,
344 strum::VariantArray,
345 strum::VariantNames,
346)]
347#[serde(rename_all = "snake_case")]
348pub enum NotifyWhenAgentWaiting {
349 #[default]
350 PrimaryScreen,
351 AllScreens,
352 Never,
353}
354
355#[derive(
356 Copy,
357 Clone,
358 Default,
359 Debug,
360 Serialize,
361 Deserialize,
362 JsonSchema,
363 MergeFrom,
364 PartialEq,
365 strum::VariantArray,
366 strum::VariantNames,
367)]
368#[serde(rename_all = "snake_case")]
369pub enum PlaySoundWhenAgentDone {
370 #[default]
371 Never,
372 WhenHidden,
373 Always,
374}
375
376impl PlaySoundWhenAgentDone {
377 pub fn should_play(&self, visible: bool) -> bool {
378 match self {
379 PlaySoundWhenAgentDone::Never => false,
380 PlaySoundWhenAgentDone::WhenHidden => !visible,
381 PlaySoundWhenAgentDone::Always => true,
382 }
383 }
384}
385
386#[with_fallible_options]
387#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
388pub struct LanguageModelSelection {
389 pub provider: LanguageModelProviderSetting,
390 pub model: String,
391 #[serde(default)]
392 pub enable_thinking: bool,
393 pub effort: Option<String>,
394}
395
396#[with_fallible_options]
397#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
398pub struct LanguageModelParameters {
399 pub provider: Option<LanguageModelProviderSetting>,
400 pub model: Option<String>,
401 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
402 pub temperature: Option<f32>,
403}
404
405#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
406pub struct LanguageModelProviderSetting(pub String);
407
408impl JsonSchema for LanguageModelProviderSetting {
409 fn schema_name() -> Cow<'static, str> {
410 "LanguageModelProviderSetting".into()
411 }
412
413 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
414 // list the builtin providers as a subset so that we still auto complete them in the settings
415 json_schema!({
416 "anyOf": [
417 {
418 "type": "string",
419 "enum": [
420 "amazon-bedrock",
421 "anthropic",
422 "copilot_chat",
423 "deepseek",
424 "google",
425 "lmstudio",
426 "mistral",
427 "ollama",
428 "openai",
429 "openrouter",
430 "vercel",
431 "vercel_ai_gateway",
432 "x_ai",
433 "zed.dev"
434 ]
435 },
436 {
437 "type": "string",
438 }
439 ]
440 })
441 }
442}
443
444impl From<String> for LanguageModelProviderSetting {
445 fn from(provider: String) -> Self {
446 Self(provider)
447 }
448}
449
450impl From<&str> for LanguageModelProviderSetting {
451 fn from(provider: &str) -> Self {
452 Self(provider.to_string())
453 }
454}
455
456#[with_fallible_options]
457#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
458#[serde(transparent)]
459pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
460
461impl std::ops::Deref for AllAgentServersSettings {
462 type Target = HashMap<String, CustomAgentServerSettings>;
463
464 fn deref(&self) -> &Self::Target {
465 &self.0
466 }
467}
468
469impl std::ops::DerefMut for AllAgentServersSettings {
470 fn deref_mut(&mut self) -> &mut Self::Target {
471 &mut self.0
472 }
473}
474
475#[with_fallible_options]
476#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
477#[serde(tag = "type", rename_all = "snake_case")]
478pub enum CustomAgentServerSettings {
479 Custom {
480 #[serde(rename = "command")]
481 path: PathBuf,
482 #[serde(default, skip_serializing_if = "Vec::is_empty")]
483 args: Vec<String>,
484 /// Default: {}
485 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
486 env: HashMap<String, String>,
487 /// The default mode to use for this agent.
488 ///
489 /// Note: Not only all agents support modes.
490 ///
491 /// Default: None
492 default_mode: Option<String>,
493 /// The default model to use for this agent.
494 ///
495 /// This should be the model ID as reported by the agent.
496 ///
497 /// Default: None
498 default_model: Option<String>,
499 /// The favorite models for this agent.
500 ///
501 /// These are the model IDs as reported by the agent.
502 ///
503 /// Default: []
504 #[serde(default, skip_serializing_if = "Vec::is_empty")]
505 favorite_models: Vec<String>,
506 /// Default values for session config options.
507 ///
508 /// This is a map from config option ID to value ID.
509 ///
510 /// Default: {}
511 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
512 default_config_options: HashMap<String, String>,
513 /// Favorited values for session config options.
514 ///
515 /// This is a map from config option ID to a list of favorited value IDs.
516 ///
517 /// Default: {}
518 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
519 favorite_config_option_values: HashMap<String, Vec<String>>,
520 },
521 Extension {
522 /// Additional environment variables to pass to the agent.
523 ///
524 /// Default: {}
525 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
526 env: HashMap<String, String>,
527 /// The default mode to use for this agent.
528 ///
529 /// Note: Not only all agents support modes.
530 ///
531 /// Default: None
532 default_mode: Option<String>,
533 /// The default model to use for this agent.
534 ///
535 /// This should be the model ID as reported by the agent.
536 ///
537 /// Default: None
538 default_model: Option<String>,
539 /// The favorite models for this agent.
540 ///
541 /// These are the model IDs as reported by the agent.
542 ///
543 /// Default: []
544 #[serde(default, skip_serializing_if = "Vec::is_empty")]
545 favorite_models: Vec<String>,
546 /// Default values for session config options.
547 ///
548 /// This is a map from config option ID to value ID.
549 ///
550 /// Default: {}
551 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
552 default_config_options: HashMap<String, String>,
553 /// Favorited values for session config options.
554 ///
555 /// This is a map from config option ID to a list of favorited value IDs.
556 ///
557 /// Default: {}
558 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
559 favorite_config_option_values: HashMap<String, Vec<String>>,
560 },
561 Registry {
562 /// Additional environment variables to pass to the agent.
563 ///
564 /// Default: {}
565 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
566 env: HashMap<String, String>,
567 /// The default mode to use for this agent.
568 ///
569 /// Note: Not only all agents support modes.
570 ///
571 /// Default: None
572 default_mode: Option<String>,
573 /// The default model to use for this agent.
574 ///
575 /// This should be the model ID as reported by the agent.
576 ///
577 /// Default: None
578 default_model: Option<String>,
579 /// The favorite models for this agent.
580 ///
581 /// These are the model IDs as reported by the agent.
582 ///
583 /// Default: []
584 #[serde(default, skip_serializing_if = "Vec::is_empty")]
585 favorite_models: Vec<String>,
586 /// Default values for session config options.
587 ///
588 /// This is a map from config option ID to value ID.
589 ///
590 /// Default: {}
591 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
592 default_config_options: HashMap<String, String>,
593 /// Favorited values for session config options.
594 ///
595 /// This is a map from config option ID to a list of favorited value IDs.
596 ///
597 /// Default: {}
598 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
599 favorite_config_option_values: HashMap<String, Vec<String>>,
600 },
601}
602
603#[with_fallible_options]
604#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
605pub struct ToolPermissionsContent {
606 /// Global default permission when no tool-specific rules match.
607 /// Individual tools can override this with their own default.
608 /// Default: confirm
609 #[serde(alias = "default_mode")]
610 pub default: Option<ToolPermissionMode>,
611
612 /// Per-tool permission rules.
613 /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
614 /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
615 /// even tools without meaningful text input can have a `default` set.
616 #[serde(default)]
617 pub tools: HashMap<Arc<str>, ToolRulesContent>,
618}
619
620#[with_fallible_options]
621#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
622pub struct ToolRulesContent {
623 /// Default mode when no regex rules match.
624 /// When unset, inherits from the global `tool_permissions.default`.
625 #[serde(alias = "default_mode")]
626 pub default: Option<ToolPermissionMode>,
627
628 /// Regexes for inputs to auto-approve.
629 /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
630 /// For `copy_path` and `move_path`, patterns are matched independently against each
631 /// path (source and destination).
632 /// Patterns accumulate across settings layers (user, project, profile) and cannot be
633 /// removed by a higher-priority layer—only new patterns can be added.
634 /// Default: []
635 pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
636
637 /// Regexes for inputs to auto-reject.
638 /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
639 /// For `copy_path` and `move_path`, patterns are matched independently against each
640 /// path (source and destination).
641 /// Patterns accumulate across settings layers (user, project, profile) and cannot be
642 /// removed by a higher-priority layer—only new patterns can be added.
643 /// Default: []
644 pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
645
646 /// Regexes for inputs that must always prompt.
647 /// Takes precedence over always_allow but not always_deny.
648 /// For `copy_path` and `move_path`, patterns are matched independently against each
649 /// path (source and destination).
650 /// Patterns accumulate across settings layers (user, project, profile) and cannot be
651 /// removed by a higher-priority layer—only new patterns can be added.
652 /// Default: []
653 pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
654}
655
656#[with_fallible_options]
657#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
658pub struct ToolRegexRule {
659 /// The regex pattern to match.
660 #[serde(default)]
661 pub pattern: String,
662
663 /// Whether the regex is case-sensitive.
664 /// Default: false (case-insensitive)
665 pub case_sensitive: Option<bool>,
666}
667
668#[derive(
669 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
670)]
671#[serde(rename_all = "snake_case")]
672pub enum ToolPermissionMode {
673 /// Auto-approve without prompting.
674 Allow,
675 /// Auto-reject with an error.
676 Deny,
677 /// Always prompt for confirmation (default behavior).
678 #[default]
679 Confirm,
680}
681
682impl std::fmt::Display for ToolPermissionMode {
683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 match self {
685 ToolPermissionMode::Allow => write!(f, "Allow"),
686 ToolPermissionMode::Deny => write!(f, "Deny"),
687 ToolPermissionMode::Confirm => write!(f, "Confirm"),
688 }
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn test_set_tool_default_permission_creates_structure() {
698 let mut settings = AgentSettingsContent::default();
699 assert!(settings.tool_permissions.is_none());
700
701 settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
702
703 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
704 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
705 assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
706 }
707
708 #[test]
709 fn test_set_tool_default_permission_updates_existing() {
710 let mut settings = AgentSettingsContent::default();
711
712 settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
713 settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
714
715 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
716 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
717 assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
718 }
719
720 #[test]
721 fn test_set_tool_default_permission_for_mcp_tool() {
722 let mut settings = AgentSettingsContent::default();
723
724 settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);
725
726 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
727 let mcp_rules = tool_permissions
728 .tools
729 .get("mcp:github:create_issue")
730 .unwrap();
731 assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
732 }
733
734 #[test]
735 fn test_add_tool_allow_pattern_creates_structure() {
736 let mut settings = AgentSettingsContent::default();
737 assert!(settings.tool_permissions.is_none());
738
739 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
740
741 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
742 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
743 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
744 assert_eq!(always_allow.0.len(), 1);
745 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
746 }
747
748 #[test]
749 fn test_add_tool_allow_pattern_appends_to_existing() {
750 let mut settings = AgentSettingsContent::default();
751
752 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
753 settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
754
755 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
756 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
757 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
758 assert_eq!(always_allow.0.len(), 2);
759 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
760 assert_eq!(always_allow.0[1].pattern, "^npm\\s");
761 }
762
763 #[test]
764 fn test_add_tool_allow_pattern_does_not_duplicate() {
765 let mut settings = AgentSettingsContent::default();
766
767 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
768 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
769 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
770
771 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
772 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
773 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
774 assert_eq!(
775 always_allow.0.len(),
776 1,
777 "Duplicate patterns should not be added"
778 );
779 }
780
781 #[test]
782 fn test_add_tool_allow_pattern_for_different_tools() {
783 let mut settings = AgentSettingsContent::default();
784
785 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
786 settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
787
788 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
789
790 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
791 assert_eq!(
792 terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
793 "^cargo\\s"
794 );
795
796 let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
797 assert_eq!(
798 fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
799 "^https?://github\\.com"
800 );
801 }
802
803 #[test]
804 fn test_add_tool_deny_pattern_creates_structure() {
805 let mut settings = AgentSettingsContent::default();
806 assert!(settings.tool_permissions.is_none());
807
808 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
809
810 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
811 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
812 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
813 assert_eq!(always_deny.0.len(), 1);
814 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
815 }
816
817 #[test]
818 fn test_add_tool_deny_pattern_appends_to_existing() {
819 let mut settings = AgentSettingsContent::default();
820
821 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
822 settings.add_tool_deny_pattern("terminal", "^sudo\\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 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
827 assert_eq!(always_deny.0.len(), 2);
828 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
829 assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
830 }
831
832 #[test]
833 fn test_add_tool_deny_pattern_does_not_duplicate() {
834 let mut settings = AgentSettingsContent::default();
835
836 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
837 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
838 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
839
840 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
841 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
842 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
843 assert_eq!(
844 always_deny.0.len(),
845 1,
846 "Duplicate patterns should not be added"
847 );
848 }
849
850 #[test]
851 fn test_add_tool_deny_and_allow_patterns_separate() {
852 let mut settings = AgentSettingsContent::default();
853
854 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
855 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
856
857 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
858 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
859
860 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
861 assert_eq!(always_allow.0.len(), 1);
862 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
863
864 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
865 assert_eq!(always_deny.0.len(), 1);
866 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
867 }
868}