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