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