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