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