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