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