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