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