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