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