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