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