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