Coverage for agentos/guardrails/rules.py: 0%
46 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 10:59 +0800
1"""
2Built-in guardrail rules — PII detection, keyword blocking, length limits, regex,
3toxicity heuristics, and code injection detection.
4"""
6import re
8from agentos.guardrails.engine import GuardrailAction, GuardrailCategory, GuardrailRule
11def PIIRule( # noqa: N802
12 name: str = "pii_detector",
13 action: GuardrailAction = GuardrailAction.SANITIZE,
14 enabled: bool = True,
15) -> GuardrailRule:
16 """Detects common PII patterns (email, phone, SSN, credit card) and redacts."""
18 _pii_patterns = [
19 (r"\b[\w._%+-]+@[\w.-]+\.[a-zA-Z]{2,}\b", "[EMAIL]"),
20 (r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "[PHONE]"),
21 (r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]"),
22 (r"\b(?:\d{4}[- ]?){3}\d{4}\b", "[CARD]"),
23 ]
25 def _check(text: str) -> bool:
26 for pat, _ in _pii_patterns:
27 if re.search(pat, text):
28 return True
29 return False
31 def _sanitize(text: str) -> str:
32 for pat, repl in _pii_patterns:
33 text = re.sub(pat, repl, text)
34 return text
36 return GuardrailRule(
37 name=name,
38 category=GuardrailCategory.PII,
39 action=action,
40 check=_check,
41 sanitize=_sanitize,
42 description="Redacts emails, phone numbers, SSNs, and credit card numbers.",
43 enabled=enabled,
44 )
47def KeywordBlockRule( # noqa: N802
48 keywords: list[str],
49 name: str = "keyword_block",
50 case_sensitive: bool = False,
51 enabled: bool = True,
52) -> GuardrailRule:
53 """Blocks text containing any of the specified keywords."""
55 _kw = keywords if case_sensitive else [kw.lower() for kw in keywords]
57 def _check(text: str) -> bool:
58 t = text if case_sensitive else text.lower()
59 return any(kw in t for kw in _kw)
61 return GuardrailRule(
62 name=name,
63 category=GuardrailCategory.KEYWORD,
64 action=GuardrailAction.BLOCK,
65 check=_check,
66 description=f"Blocks content containing: {', '.join(keywords[:5])}",
67 enabled=enabled,
68 )
71def LengthLimitRule( # noqa: N802
72 max_input: int = 32_000,
73 max_output: int = 16_000,
74 name: str = "length_limit",
75 enabled: bool = True,
76) -> GuardrailRule:
77 """Blocks text exceeding length limits (input or output)."""
79 def _check(text: str) -> bool:
80 return len(text) > max(max_input, max_output)
82 return GuardrailRule(
83 name=name,
84 category=GuardrailCategory.LENGTH,
85 action=GuardrailAction.BLOCK,
86 check=_check,
87 description=f"Limits input to {max_input} chars, output to {max_output} chars.",
88 enabled=enabled,
89 )
92def RegexRule( # noqa: N802
93 pattern: str,
94 name: str = "regex_rule",
95 action: GuardrailAction = GuardrailAction.FLAG,
96 description: str = "",
97 enabled: bool = True,
98) -> GuardrailRule:
99 """Flags or blocks text matching a custom regex pattern."""
100 _pat = re.compile(pattern)
102 def _check(text: str) -> bool:
103 return bool(_pat.search(text))
105 return GuardrailRule(
106 name=name,
107 category=GuardrailCategory.CUSTOM,
108 action=action,
109 check=_check,
110 description=description or f"Regex: {pattern[:40]}",
111 enabled=enabled,
112 )
115def ToxicityRule( # noqa: N802
116 name: str = "toxicity_check",
117 action: GuardrailAction = GuardrailAction.FLAG,
118 enabled: bool = True,
119) -> GuardrailRule:
120 """Heuristic toxicity detection via keyword lists (offline, no API call)."""
122 _toxic = [
123 "kill yourself",
124 "kys",
125 "die in a fire",
126 "i hope you die",
127 "nigger",
128 "faggot",
129 "retard",
130 "cunt",
131 "terrorist",
132 "bomb making",
133 "how to make a bomb",
134 "child porn",
135 "cp ",
136 "lolicon",
137 ]
139 def _check(text: str) -> bool:
140 t = text.lower()
141 return any(toxic in t for toxic in _toxic)
143 return GuardrailRule(
144 name=name,
145 category=GuardrailCategory.TOXICITY,
146 action=action,
147 check=_check,
148 description="Flags text containing toxic or harmful language.",
149 enabled=enabled,
150 )
153def CodeInjectionRule( # noqa: N802
154 name: str = "code_injection_detector",
155 action: GuardrailAction = GuardrailAction.BLOCK,
156 enabled: bool = True,
157) -> GuardrailRule:
158 """Detects prompt injection and code injection patterns."""
160 _patterns = [
161 r"ignore (all )?(previous|above|prior) (instructions?|prompts?)",
162 r"forget (your|all) (instructions?|rules?|training)",
163 r"you are now (DAN|developer mode|jailbroken)",
164 r"system:\s*you are",
165 r"<\|im_start\|>",
166 r"<\|system\|>",
167 r"```.*\b(?:rm\s+-rf|DROP\s+TABLE|DELETE\s+FROM|shutdown)\b",
168 r"\b(?:DROP\s+TABLE|DELETE\s+FROM|TRUNCATE\s+TABLE|ALTER\s+TABLE)\b",
169 r"\brm\s+-rf\s+/",
170 r"\bexec\s*\(.*\)",
171 r"\beval\s*\(.*\)",
172 r"\b__import__\s*\(.*\)",
173 r"\bimportlib\.import_module\b",
174 ]
176 _compiled = [re.compile(p, re.IGNORECASE) for p in _patterns]
178 def _check(text: str) -> bool:
179 return any(pat.search(text) for pat in _compiled)
181 return GuardrailRule(
182 name=name,
183 category=GuardrailCategory.INJECTION,
184 action=action,
185 check=_check,
186 description="Blocks prompt injection and code injection attempts.",
187 enabled=enabled,
188 )
191def build_default_rules(
192 blocked_keywords: list[str] | None = None,
193 max_input_length: int = 32_000,
194 max_output_length: int = 16_000,
195) -> list[GuardrailRule]:
196 """Build a sensible default rule set for production use."""
197 rules: list[GuardrailRule] = [
198 CodeInjectionRule(),
199 PIIRule(),
200 ToxicityRule(action=GuardrailAction.FLAG),
201 LengthLimitRule(max_input=max_input_length, max_output=max_output_length),
202 ]
203 if blocked_keywords:
204 rules.append(KeywordBlockRule(keywords=blocked_keywords))
205 return rules