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 clicking the stop button on a running terminal tool should also cancel the agent's generation.
113 /// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
114 ///
115 /// Default: true
116 pub cancel_generation_on_terminal_stop: 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#[with_fallible_options]
302#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
303pub struct LanguageModelParameters {
304 pub provider: Option<LanguageModelProviderSetting>,
305 pub model: Option<String>,
306 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
307 pub temperature: Option<f32>,
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
311pub struct LanguageModelProviderSetting(pub String);
312
313impl JsonSchema for LanguageModelProviderSetting {
314 fn schema_name() -> Cow<'static, str> {
315 "LanguageModelProviderSetting".into()
316 }
317
318 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
319 // list the builtin providers as a subset so that we still auto complete them in the settings
320 json_schema!({
321 "anyOf": [
322 {
323 "type": "string",
324 "enum": [
325 "amazon-bedrock",
326 "anthropic",
327 "copilot_chat",
328 "deepseek",
329 "google",
330 "lmstudio",
331 "mistral",
332 "ollama",
333 "openai",
334 "openrouter",
335 "vercel",
336 "x_ai",
337 "zed.dev"
338 ]
339 },
340 {
341 "type": "string",
342 }
343 ]
344 })
345 }
346}
347
348impl From<String> for LanguageModelProviderSetting {
349 fn from(provider: String) -> Self {
350 Self(provider)
351 }
352}
353
354impl From<&str> for LanguageModelProviderSetting {
355 fn from(provider: &str) -> Self {
356 Self(provider.to_string())
357 }
358}
359
360#[with_fallible_options]
361#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
362pub struct AllAgentServersSettings {
363 pub gemini: Option<BuiltinAgentServerSettings>,
364 pub claude: Option<BuiltinAgentServerSettings>,
365 pub codex: Option<BuiltinAgentServerSettings>,
366
367 /// Custom agent servers configured by the user
368 #[serde(flatten)]
369 pub custom: HashMap<String, CustomAgentServerSettings>,
370}
371
372#[with_fallible_options]
373#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
374pub struct BuiltinAgentServerSettings {
375 /// Absolute path to a binary to be used when launching this agent.
376 ///
377 /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
378 #[serde(rename = "command")]
379 pub path: Option<PathBuf>,
380 /// If a binary is specified in `command`, it will be passed these arguments.
381 pub args: Option<Vec<String>>,
382 /// If a binary is specified in `command`, it will be passed these environment variables.
383 pub env: Option<HashMap<String, String>>,
384 /// Whether to skip searching `$PATH` for an agent server binary when
385 /// launching this agent.
386 ///
387 /// This has no effect if a `command` is specified. Otherwise, when this is
388 /// `false`, Zed will search `$PATH` for an agent server binary and, if one
389 /// is found, use it for threads with this agent. If no agent binary is
390 /// found on `$PATH`, Zed will automatically install and use its own binary.
391 /// When this is `true`, Zed will not search `$PATH`, and will always use
392 /// its own binary.
393 ///
394 /// Default: true
395 pub ignore_system_version: Option<bool>,
396 /// The default mode to use for this agent.
397 ///
398 /// Note: Not only all agents support modes.
399 ///
400 /// Default: None
401 pub default_mode: Option<String>,
402 /// The default model to use for this agent.
403 ///
404 /// This should be the model ID as reported by the agent.
405 ///
406 /// Default: None
407 pub default_model: Option<String>,
408 /// The favorite models for this agent.
409 ///
410 /// These are the model IDs as reported by the agent.
411 ///
412 /// Default: []
413 #[serde(default, skip_serializing_if = "Vec::is_empty")]
414 pub favorite_models: Vec<String>,
415 /// Default values for session config options.
416 ///
417 /// This is a map from config option ID to value ID.
418 ///
419 /// Default: {}
420 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
421 pub default_config_options: HashMap<String, String>,
422 /// Favorited values for session config options.
423 ///
424 /// This is a map from config option ID to a list of favorited value IDs.
425 ///
426 /// Default: {}
427 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
428 pub favorite_config_option_values: HashMap<String, Vec<String>>,
429}
430
431#[with_fallible_options]
432#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
433#[serde(tag = "type", rename_all = "snake_case")]
434pub enum CustomAgentServerSettings {
435 Custom {
436 #[serde(rename = "command")]
437 path: PathBuf,
438 #[serde(default, skip_serializing_if = "Vec::is_empty")]
439 args: Vec<String>,
440 /// Default: {}
441 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
442 env: HashMap<String, String>,
443 /// The default mode to use for this agent.
444 ///
445 /// Note: Not only all agents support modes.
446 ///
447 /// Default: None
448 default_mode: Option<String>,
449 /// The default model to use for this agent.
450 ///
451 /// This should be the model ID as reported by the agent.
452 ///
453 /// Default: None
454 default_model: Option<String>,
455 /// The favorite models for this agent.
456 ///
457 /// These are the model IDs as reported by the agent.
458 ///
459 /// Default: []
460 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 favorite_models: Vec<String>,
462 /// Default values for session config options.
463 ///
464 /// This is a map from config option ID to value ID.
465 ///
466 /// Default: {}
467 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
468 default_config_options: HashMap<String, String>,
469 /// Favorited values for session config options.
470 ///
471 /// This is a map from config option ID to a list of favorited value IDs.
472 ///
473 /// Default: {}
474 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
475 favorite_config_option_values: HashMap<String, Vec<String>>,
476 },
477 Extension {
478 /// Additional environment variables to pass to the agent.
479 ///
480 /// Default: {}
481 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
482 env: HashMap<String, String>,
483 /// The default mode to use for this agent.
484 ///
485 /// Note: Not only all agents support modes.
486 ///
487 /// Default: None
488 default_mode: Option<String>,
489 /// The default model to use for this agent.
490 ///
491 /// This should be the model ID as reported by the agent.
492 ///
493 /// Default: None
494 default_model: Option<String>,
495 /// The favorite models for this agent.
496 ///
497 /// These are the model IDs as reported by the agent.
498 ///
499 /// Default: []
500 #[serde(default, skip_serializing_if = "Vec::is_empty")]
501 favorite_models: Vec<String>,
502 /// Default values for session config options.
503 ///
504 /// This is a map from config option ID to value ID.
505 ///
506 /// Default: {}
507 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
508 default_config_options: HashMap<String, String>,
509 /// Favorited values for session config options.
510 ///
511 /// This is a map from config option ID to a list of favorited value IDs.
512 ///
513 /// Default: {}
514 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
515 favorite_config_option_values: HashMap<String, Vec<String>>,
516 },
517 Registry {
518 /// Additional environment variables to pass to the agent.
519 ///
520 /// Default: {}
521 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
522 env: HashMap<String, String>,
523 /// The default mode to use for this agent.
524 ///
525 /// Note: Not only all agents support modes.
526 ///
527 /// Default: None
528 default_mode: Option<String>,
529 /// The default model to use for this agent.
530 ///
531 /// This should be the model ID as reported by the agent.
532 ///
533 /// Default: None
534 default_model: Option<String>,
535 /// The favorite models for this agent.
536 ///
537 /// These are the model IDs as reported by the agent.
538 ///
539 /// Default: []
540 #[serde(default, skip_serializing_if = "Vec::is_empty")]
541 favorite_models: Vec<String>,
542 /// Default values for session config options.
543 ///
544 /// This is a map from config option ID to value ID.
545 ///
546 /// Default: {}
547 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
548 default_config_options: HashMap<String, String>,
549 /// Favorited values for session config options.
550 ///
551 /// This is a map from config option ID to a list of favorited value IDs.
552 ///
553 /// Default: {}
554 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
555 favorite_config_option_values: HashMap<String, Vec<String>>,
556 },
557}
558
559#[with_fallible_options]
560#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
561pub struct ToolPermissionsContent {
562 /// Per-tool permission rules.
563 /// Keys: terminal, edit_file, delete_path, move_path, create_directory,
564 /// save_file, fetch, web_search
565 #[serde(default)]
566 pub tools: HashMap<Arc<str>, ToolRulesContent>,
567}
568
569#[with_fallible_options]
570#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
571pub struct ToolRulesContent {
572 /// Default mode when no regex rules match.
573 /// Default: confirm
574 pub default_mode: Option<ToolPermissionMode>,
575
576 /// Regexes for inputs to auto-approve.
577 /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
578 /// Default: []
579 pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
580
581 /// Regexes for inputs to auto-reject.
582 /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
583 /// Default: []
584 pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
585
586 /// Regexes for inputs that must always prompt.
587 /// Takes precedence over always_allow but not always_deny.
588 /// Default: []
589 pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
590}
591
592#[with_fallible_options]
593#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
594pub struct ToolRegexRule {
595 /// The regex pattern to match.
596 #[serde(default)]
597 pub pattern: String,
598
599 /// Whether the regex is case-sensitive.
600 /// Default: false (case-insensitive)
601 pub case_sensitive: Option<bool>,
602}
603
604#[derive(
605 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
606)]
607#[serde(rename_all = "snake_case")]
608pub enum ToolPermissionMode {
609 /// Auto-approve without prompting.
610 Allow,
611 /// Auto-reject with an error.
612 Deny,
613 /// Always prompt for confirmation (default behavior).
614 #[default]
615 Confirm,
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_set_tool_default_mode_creates_structure() {
624 let mut settings = AgentSettingsContent::default();
625 assert!(settings.tool_permissions.is_none());
626
627 settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
628
629 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
630 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
631 assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
632 }
633
634 #[test]
635 fn test_set_tool_default_mode_updates_existing() {
636 let mut settings = AgentSettingsContent::default();
637
638 settings.set_tool_default_mode("terminal", ToolPermissionMode::Confirm);
639 settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
640
641 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
642 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
643 assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
644 }
645
646 #[test]
647 fn test_set_tool_default_mode_for_mcp_tool() {
648 let mut settings = AgentSettingsContent::default();
649
650 settings.set_tool_default_mode("mcp:github:create_issue", ToolPermissionMode::Allow);
651
652 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
653 let mcp_rules = tool_permissions
654 .tools
655 .get("mcp:github:create_issue")
656 .unwrap();
657 assert_eq!(mcp_rules.default_mode, Some(ToolPermissionMode::Allow));
658 }
659
660 #[test]
661 fn test_add_tool_allow_pattern_creates_structure() {
662 let mut settings = AgentSettingsContent::default();
663 assert!(settings.tool_permissions.is_none());
664
665 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
666
667 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
668 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
669 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
670 assert_eq!(always_allow.0.len(), 1);
671 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
672 }
673
674 #[test]
675 fn test_add_tool_allow_pattern_appends_to_existing() {
676 let mut settings = AgentSettingsContent::default();
677
678 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
679 settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
680
681 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
682 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
683 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
684 assert_eq!(always_allow.0.len(), 2);
685 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
686 assert_eq!(always_allow.0[1].pattern, "^npm\\s");
687 }
688
689 #[test]
690 fn test_add_tool_allow_pattern_does_not_duplicate() {
691 let mut settings = AgentSettingsContent::default();
692
693 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
694 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
695 settings.add_tool_allow_pattern("terminal", "^cargo\\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_allow = terminal_rules.always_allow.as_ref().unwrap();
700 assert_eq!(
701 always_allow.0.len(),
702 1,
703 "Duplicate patterns should not be added"
704 );
705 }
706
707 #[test]
708 fn test_add_tool_allow_pattern_for_different_tools() {
709 let mut settings = AgentSettingsContent::default();
710
711 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
712 settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
713
714 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
715
716 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
717 assert_eq!(
718 terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
719 "^cargo\\s"
720 );
721
722 let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
723 assert_eq!(
724 fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
725 "^https?://github\\.com"
726 );
727 }
728
729 #[test]
730 fn test_add_tool_deny_pattern_creates_structure() {
731 let mut settings = AgentSettingsContent::default();
732 assert!(settings.tool_permissions.is_none());
733
734 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
735
736 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
737 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
738 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
739 assert_eq!(always_deny.0.len(), 1);
740 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
741 }
742
743 #[test]
744 fn test_add_tool_deny_pattern_appends_to_existing() {
745 let mut settings = AgentSettingsContent::default();
746
747 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
748 settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
749
750 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
751 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
752 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
753 assert_eq!(always_deny.0.len(), 2);
754 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
755 assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
756 }
757
758 #[test]
759 fn test_add_tool_deny_pattern_does_not_duplicate() {
760 let mut settings = AgentSettingsContent::default();
761
762 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
763 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
764 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
765
766 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
767 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
768 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
769 assert_eq!(
770 always_deny.0.len(),
771 1,
772 "Duplicate patterns should not be added"
773 );
774 }
775
776 #[test]
777 fn test_add_tool_deny_and_allow_patterns_separate() {
778 let mut settings = AgentSettingsContent::default();
779
780 settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
781 settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
782
783 let tool_permissions = settings.tool_permissions.as_ref().unwrap();
784 let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
785
786 let always_allow = terminal_rules.always_allow.as_ref().unwrap();
787 assert_eq!(always_allow.0.len(), 1);
788 assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
789
790 let always_deny = terminal_rules.always_deny.as_ref().unwrap();
791 assert_eq!(always_deny.0.len(), 1);
792 assert_eq!(always_deny.0[0].pattern, "^rm\\s");
793 }
794}