1mod agent_profile;
2
3use std::sync::Arc;
4
5use agent_client_protocol::ModelId;
6use collections::{HashSet, IndexMap};
7use gpui::{App, Pixels, px};
8use language_model::LanguageModel;
9use project::DisableAiSettings;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use settings::{
13 DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
14 NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode,
15};
16
17pub use crate::agent_profile::*;
18
19pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");
20pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
21 include_str!("prompts/summarize_thread_detailed_prompt.txt");
22
23#[derive(Clone, Debug, RegisterSetting)]
24pub struct AgentSettings {
25 pub enabled: bool,
26 pub button: bool,
27 pub dock: DockPosition,
28 pub agents_panel_dock: DockSide,
29 pub default_width: Pixels,
30 pub default_height: Pixels,
31 pub default_model: Option<LanguageModelSelection>,
32 pub inline_assistant_model: Option<LanguageModelSelection>,
33 pub inline_assistant_use_streaming_tools: bool,
34 pub commit_message_model: Option<LanguageModelSelection>,
35 pub thread_summary_model: Option<LanguageModelSelection>,
36 pub inline_alternatives: Vec<LanguageModelSelection>,
37 pub favorite_models: Vec<LanguageModelSelection>,
38 pub default_profile: AgentProfileId,
39 pub default_view: DefaultAgentView,
40 pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
41 pub always_allow_tool_actions: bool,
42 pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
43 pub play_sound_when_agent_done: bool,
44 pub single_file_review: bool,
45 pub model_parameters: Vec<LanguageModelParameters>,
46 pub enable_feedback: bool,
47 pub expand_edit_card: bool,
48 pub expand_terminal_card: bool,
49 pub cancel_generation_on_terminal_stop: bool,
50 pub use_modifier_to_send: bool,
51 pub message_editor_min_lines: usize,
52 pub show_turn_stats: bool,
53 pub tool_permissions: ToolPermissions,
54}
55
56impl AgentSettings {
57 pub fn enabled(&self, cx: &App) -> bool {
58 self.enabled && !DisableAiSettings::get_global(cx).disable_ai
59 }
60
61 pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
62 let settings = Self::get_global(cx);
63 for setting in settings.model_parameters.iter().rev() {
64 if let Some(provider) = &setting.provider
65 && provider.0 != model.provider_id().0
66 {
67 continue;
68 }
69 if let Some(setting_model) = &setting.model
70 && *setting_model != model.id().0
71 {
72 continue;
73 }
74 return setting.temperature;
75 }
76 return None;
77 }
78
79 pub fn set_message_editor_max_lines(&self) -> usize {
80 self.message_editor_min_lines * 2
81 }
82
83 pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
84 self.favorite_models
85 .iter()
86 .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
87 .collect()
88 }
89}
90
91#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
92pub struct AgentProfileId(pub Arc<str>);
93
94impl AgentProfileId {
95 pub fn as_str(&self) -> &str {
96 &self.0
97 }
98}
99
100impl std::fmt::Display for AgentProfileId {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 write!(f, "{}", self.0)
103 }
104}
105
106impl Default for AgentProfileId {
107 fn default() -> Self {
108 Self("write".into())
109 }
110}
111
112#[derive(Clone, Debug, Default)]
113pub struct ToolPermissions {
114 pub tools: collections::HashMap<Arc<str>, ToolRules>,
115}
116
117impl ToolPermissions {
118 /// Returns all invalid regex patterns across all tools.
119 pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> {
120 self.tools
121 .values()
122 .flat_map(|rules| rules.invalid_patterns.iter())
123 .collect()
124 }
125
126 /// Returns true if any tool has invalid regex patterns.
127 pub fn has_invalid_patterns(&self) -> bool {
128 self.tools
129 .values()
130 .any(|rules| !rules.invalid_patterns.is_empty())
131 }
132}
133
134/// Represents a regex pattern that failed to compile.
135#[derive(Clone, Debug)]
136pub struct InvalidRegexPattern {
137 /// The pattern string that failed to compile.
138 pub pattern: String,
139 /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm").
140 pub rule_type: String,
141 /// The error message from the regex compiler.
142 pub error: String,
143}
144
145#[derive(Clone, Debug)]
146pub struct ToolRules {
147 pub default_mode: ToolPermissionMode,
148 pub always_allow: Vec<CompiledRegex>,
149 pub always_deny: Vec<CompiledRegex>,
150 pub always_confirm: Vec<CompiledRegex>,
151 /// Patterns that failed to compile. If non-empty, tool calls should be blocked.
152 pub invalid_patterns: Vec<InvalidRegexPattern>,
153}
154
155impl Default for ToolRules {
156 fn default() -> Self {
157 Self {
158 default_mode: ToolPermissionMode::Confirm,
159 always_allow: Vec::new(),
160 always_deny: Vec::new(),
161 always_confirm: Vec::new(),
162 invalid_patterns: Vec::new(),
163 }
164 }
165}
166
167#[derive(Clone)]
168pub struct CompiledRegex {
169 pub pattern: String,
170 pub case_sensitive: bool,
171 pub regex: regex::Regex,
172}
173
174impl std::fmt::Debug for CompiledRegex {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("CompiledRegex")
177 .field("pattern", &self.pattern)
178 .field("case_sensitive", &self.case_sensitive)
179 .finish()
180 }
181}
182
183impl CompiledRegex {
184 pub fn new(pattern: &str, case_sensitive: bool) -> Option<Self> {
185 Self::try_new(pattern, case_sensitive).ok()
186 }
187
188 pub fn try_new(pattern: &str, case_sensitive: bool) -> Result<Self, regex::Error> {
189 let regex = regex::RegexBuilder::new(pattern)
190 .case_insensitive(!case_sensitive)
191 .build()?;
192 Ok(Self {
193 pattern: pattern.to_string(),
194 case_sensitive,
195 regex,
196 })
197 }
198
199 pub fn is_match(&self, input: &str) -> bool {
200 self.regex.is_match(input)
201 }
202}
203
204impl Settings for AgentSettings {
205 fn from_settings(content: &settings::SettingsContent) -> Self {
206 let agent = content.agent.clone().unwrap();
207 Self {
208 enabled: agent.enabled.unwrap(),
209 button: agent.button.unwrap(),
210 dock: agent.dock.unwrap(),
211 agents_panel_dock: agent.agents_panel_dock.unwrap(),
212 default_width: px(agent.default_width.unwrap()),
213 default_height: px(agent.default_height.unwrap()),
214 default_model: Some(agent.default_model.unwrap()),
215 inline_assistant_model: agent.inline_assistant_model,
216 inline_assistant_use_streaming_tools: agent
217 .inline_assistant_use_streaming_tools
218 .unwrap_or(true),
219 commit_message_model: agent.commit_message_model,
220 thread_summary_model: agent.thread_summary_model,
221 inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
222 favorite_models: agent.favorite_models,
223 default_profile: AgentProfileId(agent.default_profile.unwrap()),
224 default_view: agent.default_view.unwrap(),
225 profiles: agent
226 .profiles
227 .unwrap()
228 .into_iter()
229 .map(|(key, val)| (AgentProfileId(key), val.into()))
230 .collect(),
231 always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(),
232 notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(),
233 play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(),
234 single_file_review: agent.single_file_review.unwrap(),
235 model_parameters: agent.model_parameters,
236 enable_feedback: agent.enable_feedback.unwrap(),
237 expand_edit_card: agent.expand_edit_card.unwrap(),
238 expand_terminal_card: agent.expand_terminal_card.unwrap(),
239 cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(),
240 use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
241 message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
242 show_turn_stats: agent.show_turn_stats.unwrap(),
243 tool_permissions: compile_tool_permissions(agent.tool_permissions),
244 }
245 }
246}
247
248fn compile_tool_permissions(content: Option<settings::ToolPermissionsContent>) -> ToolPermissions {
249 let Some(content) = content else {
250 return ToolPermissions::default();
251 };
252
253 let tools = content
254 .tools
255 .into_iter()
256 .map(|(tool_name, rules_content)| {
257 let mut invalid_patterns = Vec::new();
258
259 let (always_allow, allow_errors) = compile_regex_rules(
260 rules_content.always_allow.map(|v| v.0).unwrap_or_default(),
261 "always_allow",
262 );
263 invalid_patterns.extend(allow_errors);
264
265 let (always_deny, deny_errors) = compile_regex_rules(
266 rules_content.always_deny.map(|v| v.0).unwrap_or_default(),
267 "always_deny",
268 );
269 invalid_patterns.extend(deny_errors);
270
271 let (always_confirm, confirm_errors) = compile_regex_rules(
272 rules_content
273 .always_confirm
274 .map(|v| v.0)
275 .unwrap_or_default(),
276 "always_confirm",
277 );
278 invalid_patterns.extend(confirm_errors);
279
280 // Log invalid patterns for debugging. Users will see an error when they
281 // attempt to use a tool with invalid patterns in their settings.
282 for invalid in &invalid_patterns {
283 log::error!(
284 "Invalid regex pattern in tool_permissions for '{}' tool ({}): '{}' - {}",
285 tool_name,
286 invalid.rule_type,
287 invalid.pattern,
288 invalid.error,
289 );
290 }
291
292 let rules = ToolRules {
293 default_mode: rules_content.default_mode.unwrap_or_default(),
294 always_allow,
295 always_deny,
296 always_confirm,
297 invalid_patterns,
298 };
299 (tool_name, rules)
300 })
301 .collect();
302
303 ToolPermissions { tools }
304}
305
306fn compile_regex_rules(
307 rules: Vec<settings::ToolRegexRule>,
308 rule_type: &str,
309) -> (Vec<CompiledRegex>, Vec<InvalidRegexPattern>) {
310 let mut compiled = Vec::new();
311 let mut errors = Vec::new();
312
313 for rule in rules {
314 let case_sensitive = rule.case_sensitive.unwrap_or(false);
315 match CompiledRegex::try_new(&rule.pattern, case_sensitive) {
316 Ok(regex) => compiled.push(regex),
317 Err(error) => {
318 errors.push(InvalidRegexPattern {
319 pattern: rule.pattern,
320 rule_type: rule_type.to_string(),
321 error: error.to_string(),
322 });
323 }
324 }
325 }
326
327 (compiled, errors)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use serde_json::json;
334 use settings::ToolPermissionsContent;
335
336 #[test]
337 fn test_compiled_regex_case_insensitive() {
338 let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap();
339 assert!(regex.is_match("rm -rf /"));
340 assert!(regex.is_match("RM -RF /"));
341 assert!(regex.is_match("Rm -Rf /"));
342 }
343
344 #[test]
345 fn test_compiled_regex_case_sensitive() {
346 let regex = CompiledRegex::new("DROP\\s+TABLE", true).unwrap();
347 assert!(regex.is_match("DROP TABLE users"));
348 assert!(!regex.is_match("drop table users"));
349 }
350
351 #[test]
352 fn test_invalid_regex_returns_none() {
353 let result = CompiledRegex::new("[invalid(regex", false);
354 assert!(result.is_none());
355 }
356
357 #[test]
358 fn test_tool_permissions_parsing() {
359 let json = json!({
360 "tools": {
361 "terminal": {
362 "default_mode": "allow",
363 "always_deny": [
364 { "pattern": "rm\\s+-rf" }
365 ],
366 "always_allow": [
367 { "pattern": "^git\\s" }
368 ]
369 }
370 }
371 });
372
373 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
374 let permissions = compile_tool_permissions(Some(content));
375
376 let terminal_rules = permissions.tools.get("terminal").unwrap();
377 assert_eq!(terminal_rules.default_mode, ToolPermissionMode::Allow);
378 assert_eq!(terminal_rules.always_deny.len(), 1);
379 assert_eq!(terminal_rules.always_allow.len(), 1);
380 assert!(terminal_rules.always_deny[0].is_match("rm -rf /"));
381 assert!(terminal_rules.always_allow[0].is_match("git status"));
382 }
383
384 #[test]
385 fn test_tool_rules_default_mode() {
386 let json = json!({
387 "tools": {
388 "edit_file": {
389 "default_mode": "deny"
390 }
391 }
392 });
393
394 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
395 let permissions = compile_tool_permissions(Some(content));
396
397 let rules = permissions.tools.get("edit_file").unwrap();
398 assert_eq!(rules.default_mode, ToolPermissionMode::Deny);
399 }
400
401 #[test]
402 fn test_tool_permissions_empty() {
403 let permissions = compile_tool_permissions(None);
404 assert!(permissions.tools.is_empty());
405 }
406
407 #[test]
408 fn test_tool_rules_default_returns_confirm() {
409 let default_rules = ToolRules::default();
410 assert_eq!(default_rules.default_mode, ToolPermissionMode::Confirm);
411 assert!(default_rules.always_allow.is_empty());
412 assert!(default_rules.always_deny.is_empty());
413 assert!(default_rules.always_confirm.is_empty());
414 }
415
416 #[test]
417 fn test_tool_permissions_with_multiple_tools() {
418 let json = json!({
419 "tools": {
420 "terminal": {
421 "default_mode": "allow",
422 "always_deny": [{ "pattern": "rm\\s+-rf" }]
423 },
424 "edit_file": {
425 "default_mode": "confirm",
426 "always_deny": [{ "pattern": "\\.env$" }]
427 },
428 "delete_path": {
429 "default_mode": "deny"
430 }
431 }
432 });
433
434 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
435 let permissions = compile_tool_permissions(Some(content));
436
437 assert_eq!(permissions.tools.len(), 3);
438
439 let terminal = permissions.tools.get("terminal").unwrap();
440 assert_eq!(terminal.default_mode, ToolPermissionMode::Allow);
441 assert_eq!(terminal.always_deny.len(), 1);
442
443 let edit_file = permissions.tools.get("edit_file").unwrap();
444 assert_eq!(edit_file.default_mode, ToolPermissionMode::Confirm);
445 assert!(edit_file.always_deny[0].is_match("secrets.env"));
446
447 let delete_path = permissions.tools.get("delete_path").unwrap();
448 assert_eq!(delete_path.default_mode, ToolPermissionMode::Deny);
449 }
450
451 #[test]
452 fn test_tool_permissions_with_all_rule_types() {
453 let json = json!({
454 "tools": {
455 "terminal": {
456 "always_deny": [{ "pattern": "rm\\s+-rf" }],
457 "always_confirm": [{ "pattern": "sudo\\s" }],
458 "always_allow": [{ "pattern": "^git\\s+status" }]
459 }
460 }
461 });
462
463 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
464 let permissions = compile_tool_permissions(Some(content));
465
466 let terminal = permissions.tools.get("terminal").unwrap();
467 assert_eq!(terminal.always_deny.len(), 1);
468 assert_eq!(terminal.always_confirm.len(), 1);
469 assert_eq!(terminal.always_allow.len(), 1);
470
471 assert!(terminal.always_deny[0].is_match("rm -rf /"));
472 assert!(terminal.always_confirm[0].is_match("sudo apt install"));
473 assert!(terminal.always_allow[0].is_match("git status"));
474 }
475
476 #[test]
477 fn test_invalid_regex_is_tracked_and_valid_ones_still_compile() {
478 let json = json!({
479 "tools": {
480 "terminal": {
481 "always_deny": [
482 { "pattern": "[invalid(regex" },
483 { "pattern": "valid_pattern" }
484 ],
485 "always_allow": [
486 { "pattern": "[another_bad" }
487 ]
488 }
489 }
490 });
491
492 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
493 let permissions = compile_tool_permissions(Some(content));
494
495 let terminal = permissions.tools.get("terminal").unwrap();
496
497 // Valid patterns should still be compiled
498 assert_eq!(terminal.always_deny.len(), 1);
499 assert!(terminal.always_deny[0].is_match("valid_pattern"));
500
501 // Invalid patterns should be tracked (order depends on processing order)
502 assert_eq!(terminal.invalid_patterns.len(), 2);
503
504 let deny_invalid = terminal
505 .invalid_patterns
506 .iter()
507 .find(|p| p.rule_type == "always_deny")
508 .expect("should have invalid pattern from always_deny");
509 assert_eq!(deny_invalid.pattern, "[invalid(regex");
510 assert!(!deny_invalid.error.is_empty());
511
512 let allow_invalid = terminal
513 .invalid_patterns
514 .iter()
515 .find(|p| p.rule_type == "always_allow")
516 .expect("should have invalid pattern from always_allow");
517 assert_eq!(allow_invalid.pattern, "[another_bad");
518
519 // ToolPermissions helper methods should work
520 assert!(permissions.has_invalid_patterns());
521 assert_eq!(permissions.invalid_patterns().len(), 2);
522 }
523
524 #[test]
525 fn test_deny_takes_precedence_over_allow_and_confirm() {
526 let json = json!({
527 "tools": {
528 "terminal": {
529 "default_mode": "allow",
530 "always_deny": [{ "pattern": "dangerous" }],
531 "always_confirm": [{ "pattern": "dangerous" }],
532 "always_allow": [{ "pattern": "dangerous" }]
533 }
534 }
535 });
536
537 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
538 let permissions = compile_tool_permissions(Some(content));
539 let terminal = permissions.tools.get("terminal").unwrap();
540
541 assert!(
542 terminal.always_deny[0].is_match("run dangerous command"),
543 "Deny rule should match"
544 );
545 assert!(
546 terminal.always_allow[0].is_match("run dangerous command"),
547 "Allow rule should also match (but deny takes precedence at evaluation time)"
548 );
549 assert!(
550 terminal.always_confirm[0].is_match("run dangerous command"),
551 "Confirm rule should also match (but deny takes precedence at evaluation time)"
552 );
553 }
554
555 #[test]
556 fn test_confirm_takes_precedence_over_allow() {
557 let json = json!({
558 "tools": {
559 "terminal": {
560 "default_mode": "allow",
561 "always_confirm": [{ "pattern": "risky" }],
562 "always_allow": [{ "pattern": "risky" }]
563 }
564 }
565 });
566
567 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
568 let permissions = compile_tool_permissions(Some(content));
569 let terminal = permissions.tools.get("terminal").unwrap();
570
571 assert!(
572 terminal.always_confirm[0].is_match("do risky thing"),
573 "Confirm rule should match"
574 );
575 assert!(
576 terminal.always_allow[0].is_match("do risky thing"),
577 "Allow rule should also match (but confirm takes precedence at evaluation time)"
578 );
579 }
580
581 #[test]
582 fn test_regex_matches_anywhere_in_string_not_just_anchored() {
583 let json = json!({
584 "tools": {
585 "terminal": {
586 "always_deny": [
587 { "pattern": "rm\\s+-rf" },
588 { "pattern": "/etc/passwd" }
589 ]
590 }
591 }
592 });
593
594 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
595 let permissions = compile_tool_permissions(Some(content));
596 let terminal = permissions.tools.get("terminal").unwrap();
597
598 assert!(
599 terminal.always_deny[0].is_match("echo hello && rm -rf /"),
600 "Should match rm -rf in the middle of a command chain"
601 );
602 assert!(
603 terminal.always_deny[0].is_match("cd /tmp; rm -rf *"),
604 "Should match rm -rf after semicolon"
605 );
606 assert!(
607 terminal.always_deny[1].is_match("cat /etc/passwd | grep root"),
608 "Should match /etc/passwd in a pipeline"
609 );
610 assert!(
611 terminal.always_deny[1].is_match("vim /etc/passwd"),
612 "Should match /etc/passwd as argument"
613 );
614 }
615
616 #[test]
617 fn test_fork_bomb_pattern_matches() {
618 let fork_bomb_regex = CompiledRegex::new(r":\(\)\{\s*:\|:&\s*\};:", false).unwrap();
619 assert!(
620 fork_bomb_regex.is_match(":(){ :|:& };:"),
621 "Should match the classic fork bomb"
622 );
623 assert!(
624 fork_bomb_regex.is_match(":(){ :|:&};:"),
625 "Should match fork bomb without spaces"
626 );
627 }
628
629 #[test]
630 fn test_compiled_regex_stores_case_sensitivity() {
631 let case_sensitive = CompiledRegex::new("test", true).unwrap();
632 let case_insensitive = CompiledRegex::new("test", false).unwrap();
633
634 assert!(case_sensitive.case_sensitive);
635 assert!(!case_insensitive.case_sensitive);
636 }
637
638 #[test]
639 fn test_invalid_regex_is_skipped_not_fail() {
640 let json = json!({
641 "tools": {
642 "terminal": {
643 "always_deny": [
644 { "pattern": "[invalid(regex" },
645 { "pattern": "valid_pattern" }
646 ]
647 }
648 }
649 });
650
651 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
652 let permissions = compile_tool_permissions(Some(content));
653
654 let terminal = permissions.tools.get("terminal").unwrap();
655 assert_eq!(terminal.always_deny.len(), 1);
656 assert!(terminal.always_deny[0].is_match("valid_pattern"));
657 }
658
659 #[test]
660 fn test_unconfigured_tool_not_in_permissions() {
661 let json = json!({
662 "tools": {
663 "terminal": {
664 "default_mode": "allow"
665 }
666 }
667 });
668
669 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
670 let permissions = compile_tool_permissions(Some(content));
671
672 assert!(permissions.tools.contains_key("terminal"));
673 assert!(!permissions.tools.contains_key("edit_file"));
674 assert!(!permissions.tools.contains_key("fetch"));
675 }
676
677 #[test]
678 fn test_always_allow_pattern_only_matches_specified_commands() {
679 // Reproduces user-reported bug: when always_allow has pattern "^echo\s",
680 // only "echo hello" should be allowed, not "git status".
681 //
682 // User config:
683 // always_allow_tool_actions: false
684 // tool_permissions.tools.terminal.always_allow: [{ pattern: "^echo\\s" }]
685 let json = json!({
686 "tools": {
687 "terminal": {
688 "always_allow": [
689 { "pattern": "^echo\\s" }
690 ]
691 }
692 }
693 });
694
695 let content: ToolPermissionsContent = serde_json::from_value(json).unwrap();
696 let permissions = compile_tool_permissions(Some(content));
697
698 let terminal = permissions.tools.get("terminal").unwrap();
699
700 // Verify the pattern was compiled
701 assert_eq!(
702 terminal.always_allow.len(),
703 1,
704 "Should have one always_allow pattern"
705 );
706
707 // Verify the pattern matches "echo hello"
708 assert!(
709 terminal.always_allow[0].is_match("echo hello"),
710 "Pattern ^echo\\s should match 'echo hello'"
711 );
712
713 // Verify the pattern does NOT match "git status"
714 assert!(
715 !terminal.always_allow[0].is_match("git status"),
716 "Pattern ^echo\\s should NOT match 'git status'"
717 );
718
719 // Verify the pattern does NOT match "echoHello" (no space)
720 assert!(
721 !terminal.always_allow[0].is_match("echoHello"),
722 "Pattern ^echo\\s should NOT match 'echoHello' (requires whitespace)"
723 );
724
725 // Verify default_mode is Confirm (the default)
726 assert_eq!(
727 terminal.default_mode,
728 settings::ToolPermissionMode::Confirm,
729 "default_mode should be Confirm when not specified"
730 );
731 }
732}