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