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