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