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