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