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