Coverage for agentos/security/guardrails.py: 52%

154 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1""" 

2AgentOS Guardrails — Content Safety & Policy Enforcement Layer 

3━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

4 

5Production-grade guardrails system with pluggable rules, LLM-based 

6content moderation, and policy enforcement pipeline. 

7 

8Architecture: 

9 GuardrailsPipeline 

10 ├─ InputGuard (validate user input before agent processing) 

11 ├─ OutputGuard (validate agent output before returning to user) 

12 ├─ ToolGuard (validate tool calls for safety) 

13 └─ PolicyEngine (RBAC + content policy rules) 

14 

15Key Features: 

16 - Pluggable rule engine with hot-reload 

17 - LLM-based content moderation (PII/Safety/Toxicity) 

18 - Regex-based pattern matching for fast-path checks 

19 - Policy violation audit trail 

20 - Configurable block/warn/redact actions 

21""" 

22 

23from __future__ import annotations 

24 

25import re 

26import json 

27from dataclasses import dataclass, field 

28from enum import Enum 

29from typing import Any, Callable, Dict, List, Optional, Pattern 

30 

31# --------------------------------------------------------------------------- 

32# Enums & Data Classes 

33# --------------------------------------------------------------------------- 

34 

35 

36class ViolationSeverity(str, Enum): 

37 """Severity level of a guardrail violation.""" 

38 CRITICAL = "critical" # Immediate block, alert ops 

39 HIGH = "high" # Block the request 

40 MEDIUM = "medium" # Warn but allow (with redaction) 

41 LOW = "low" # Log only 

42 

43 

44class GuardAction(str, Enum): 

45 """Action to take when a guardrail is triggered.""" 

46 BLOCK = "block" # Reject the request entirely 

47 WARN = "warn" # Allow but flag with warning 

48 REDACT = "redact" # Remove offending content, allow rest 

49 LOG = "log" # Log only, no user-visible effect 

50 

51 

52class Category(str, Enum): 

53 """Standard content safety categories.""" 

54 PII = "pii" # Personally Identifiable Information 

55 TOXICITY = "toxicity" # Hate speech, harassment 

56 SELF_HARM = "self_harm" # Suicide, self-injury 

57 VIOLENCE = "violence" # Graphic violence 

58 SEXUAL = "sexual" # Explicit sexual content 

59 JAILBREAK = "jailbreak" # Prompt injection / jailbreak attempts 

60 DATA_LEAK = "data_leak" # Attempting to leak system prompts / internals 

61 MALICIOUS_CODE = "malicious_code" # Code injection, reverse shell, etc. 

62 OFF_TOPIC = "off_topic" # Outside defined scope 

63 CUSTOM = "custom" # User-defined category 

64 

65 

66@dataclass 

67class GuardViolation: 

68 """A single guardrail violation detected.""" 

69 category: Category 

70 severity: ViolationSeverity 

71 action: GuardAction 

72 message: str 

73 matched_pattern: Optional[str] = None 

74 matched_text: Optional[str] = None 

75 rule_id: Optional[str] = None 

76 metadata: Dict[str, Any] = field(default_factory=dict) 

77 

78 

79@dataclass 

80class GuardResult: 

81 """Result of running guardrails on content.""" 

82 passed: bool = True 

83 violations: List[GuardViolation] = field(default_factory=list) 

84 redacted_content: Optional[str] = None 

85 warnings: List[str] = field(default_factory=list) 

86 

87 @property 

88 def blocked(self) -> bool: 

89 return any(v.action == GuardAction.BLOCK for v in self.violations) 

90 

91 def to_dict(self) -> Dict[str, Any]: 

92 return { 

93 "passed": self.passed, 

94 "blocked": self.blocked, 

95 "violations": [ 

96 { 

97 "category": v.category.value, 

98 "severity": v.severity.value, 

99 "action": v.action.value, 

100 "message": v.message, 

101 "rule_id": v.rule_id, 

102 } 

103 for v in self.violations 

104 ], 

105 "warnings": self.warnings, 

106 } 

107 

108 

109# --------------------------------------------------------------------------- 

110# PII Detection Patterns 

111# --------------------------------------------------------------------------- 

112 

113PII_PATTERNS: Dict[str, Pattern[str]] = { 

114 "email": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"), 

115 "phone_cn": re.compile(r"1[3-9]\d{9}"), 

116 "phone_us": re.compile(r"\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"), 

117 "ssn": re.compile(r"\d{3}-\d{2}-\d{4}"), 

118 "credit_card": re.compile(r"\b(?:\d{4}[ -]?){3}\d{4}\b"), 

119 "ip_address": re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), 

120 "api_key": re.compile(r"(?:api[_-]?key|apikey|token|secret|password)\s*[:=]\s*['\"]?[\w-]{20,}['\"]?", re.IGNORECASE), 

121} 

122 

123 

124# --------------------------------------------------------------------------- 

125# Regex-based Fast-Path Rules 

126# --------------------------------------------------------------------------- 

127 

128@dataclass 

129class RegexRule: 

130 """A regex-based guardrail rule for fast-path matching.""" 

131 rule_id: str 

132 category: Category 

133 severity: ViolationSeverity 

134 action: GuardAction 

135 pattern: Pattern[str] 

136 message: str 

137 

138 

139DEFAULT_RULES: List[RegexRule] = [ 

140 # PII Rules 

141 RegexRule("pii-email", Category.PII, ViolationSeverity.HIGH, GuardAction.REDACT, 

142 PII_PATTERNS["email"], "Email address detected"), 

143 RegexRule("pii-phone-cn", Category.PII, ViolationSeverity.MEDIUM, GuardAction.REDACT, 

144 PII_PATTERNS["phone_cn"], "Chinese phone number detected"), 

145 RegexRule("pii-ssn", Category.PII, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

146 PII_PATTERNS["ssn"], "SSN detected"), 

147 RegexRule("pii-cc", Category.PII, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

148 PII_PATTERNS["credit_card"], "Credit card number detected"), 

149 RegexRule("pii-apikey", Category.PII, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

150 PII_PATTERNS["api_key"], "Potential API key in text"), 

151 

152 # Jailbreak patterns 

153 RegexRule("jb-ignore", Category.JAILBREAK, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

154 re.compile(r"(?:ignore|forget|disregard)\s+(?:all\s+)?(?:previous|above|prior)\s+(?:instructions?|prompts?|rules?)", re.IGNORECASE), 

155 "Jailbreak attempt: ignore instructions"), 

156 RegexRule("jb-dan", Category.JAILBREAK, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

157 re.compile(r"\bDAN\s*(?:mode|jailbreak)?\b", re.IGNORECASE), 

158 "Jailbreak attempt: DAN mode"), 

159 RegexRule("jb-roleplay", Category.JAILBREAK, ViolationSeverity.HIGH, GuardAction.BLOCK, 

160 re.compile(r"(?:pretend|act\s+as\s+if|imagine)\s+you\s+(?:are|were)\s+(?:an?\s+)?(?:unfiltered|unrestricted|evil|dark|malicious)", re.IGNORECASE), 

161 "Jailbreak attempt: roleplay escalation"), 

162 

163 # Malicious code 

164 RegexRule("mc-reverse-shell", Category.MALICIOUS_CODE, ViolationSeverity.CRITICAL, GuardAction.BLOCK, 

165 re.compile(r"(?:bash|sh|nc|netcat|ncat)\s+.*(?:>&?\s*/dev/(?:tcp|udp)|-e\s+/bin/(?:bash|sh))", re.IGNORECASE), 

166 "Reverse shell attempt detected"), 

167 RegexRule("mc-rm-rf", Category.MALICIOUS_CODE, ViolationSeverity.HIGH, GuardAction.BLOCK, 

168 re.compile(r"(?:rm\s+-rf|del\s+/[fsq])\s+(?:/|~|\*)", re.IGNORECASE), 

169 "Destructive file operation detected"), 

170] 

171 

172 

173# --------------------------------------------------------------------------- 

174# Guardrail Engine 

175# --------------------------------------------------------------------------- 

176 

177class RegexGuard: 

178 """Fast-path regex-based guard for common patterns.""" 

179 

180 def __init__(self, rules: Optional[List[RegexRule]] = None): 

181 self._rules: Dict[str, RegexRule] = {} 

182 for rule in (rules or DEFAULT_RULES): 

183 self._rules[rule.rule_id] = rule 

184 

185 def add_rule(self, rule: RegexRule) -> None: 

186 self._rules[rule.rule_id] = rule 

187 

188 def remove_rule(self, rule_id: str) -> None: 

189 self._rules.pop(rule_id, None) 

190 

191 def scan(self, content: str) -> List[GuardViolation]: 

192 violations: List[GuardViolation] = [] 

193 for rule in self._rules.values(): 

194 for match in rule.pattern.finditer(content): 

195 violations.append(GuardViolation( 

196 category=rule.category, 

197 severity=rule.severity, 

198 action=rule.action, 

199 message=rule.message, 

200 matched_pattern=rule.pattern.pattern, 

201 matched_text=match.group(), 

202 rule_id=rule.rule_id, 

203 )) 

204 return violations 

205 

206 def redact(self, content: str, violations: List[GuardViolation]) -> str: 

207 """Redact PII from content based on matched violations.""" 

208 result = content 

209 for v in violations: 

210 if v.action == GuardAction.REDACT and v.matched_text: 

211 category = v.category.value 

212 replacement = f"[REDACTED_{category.upper()}]" 

213 result = result.replace(v.matched_text, replacement) 

214 return result 

215 

216 

217class ContentSafetyGuard: 

218 """ 

219 LLM-based content safety assessment. 

220 

221 Uses a lightweight classifier prompt to assess content safety 

222 across multiple categories. Can be backed by any LLM provider. 

223 """ 

224 

225 SAFETY_PROMPT = """Assess the following content for safety violations. 

226Categories: toxicity, self_harm, violence, sexual, jailbreak, data_leak. 

227 

228Respond with ONLY a JSON object: 

229{ 

230 "safe": true/false, 

231 "categories": [{"category": "<name>", "severity": "low|medium|high|critical", "reason": "<brief>"}] 

232} 

233 

234Content to assess: 

235--- 

236{content} 

237---""" 

238 

239 def __init__(self, llm_call: Optional[Callable] = None): 

240 self._llm_call = llm_call 

241 

242 async def assess(self, content: str) -> List[GuardViolation]: 

243 if self._llm_call is None: 

244 return [] # No LLM backend configured, skip 

245 

246 prompt = self.SAFETY_PROMPT.format(content=content[:4000]) 

247 try: 

248 response = await self._llm_call(prompt) 

249 result = json.loads(response) 

250 except Exception: 

251 return [] 

252 

253 if result.get("safe", True): 

254 return [] 

255 

256 violations = [] 

257 severity_map = { 

258 "low": ViolationSeverity.LOW, 

259 "medium": ViolationSeverity.MEDIUM, 

260 "high": ViolationSeverity.HIGH, 

261 "critical": ViolationSeverity.CRITICAL, 

262 } 

263 for cat in result.get("categories", []): 

264 cat_name = cat.get("category", "custom") 

265 try: 

266 cat_enum = Category(cat_name) 

267 except ValueError: 

268 cat_enum = Category.CUSTOM 

269 

270 violations.append(GuardViolation( 

271 category=cat_enum, 

272 severity=severity_map.get(cat.get("severity", "medium"), ViolationSeverity.MEDIUM), 

273 action=GuardAction.BLOCK, 

274 message=cat.get("reason", f"Content safety violation: {cat_name}"), 

275 metadata={"llm_assessment": cat}, 

276 )) 

277 

278 return violations 

279 

280 

281# --------------------------------------------------------------------------- 

282# Guardrails Pipeline 

283# --------------------------------------------------------------------------- 

284 

285class GuardrailsPipeline: 

286 """ 

287 Production guardrails pipeline combining regex fast-path and LLM-based 

288 content safety assessment. 

289 

290 Usage: 

291 pipeline = GuardrailsPipeline() 

292 pipeline.add_regex_rule(...) 

293 

294 # Input validation 

295 result = await pipeline.check_input(user_message) 

296 if not result.passed: 

297 raise GuardViolationError(result) 

298 

299 # Output validation 

300 result = await pipeline.check_output(agent_response) 

301 """ 

302 

303 def __init__( 

304 self, 

305 regex_guard: Optional[RegexGuard] = None, 

306 safety_guard: Optional[ContentSafetyGuard] = None, 

307 enable_regex: bool = True, 

308 enable_safety: bool = True, 

309 ): 

310 self._regex = regex_guard or RegexGuard() 

311 self._safety = safety_guard or ContentSafetyGuard() 

312 self._enable_regex = enable_regex 

313 self._enable_safety = enable_safety 

314 self._audit_log: List[GuardResult] = [] 

315 

316 def add_regex_rule(self, rule: RegexRule) -> None: 

317 self._regex.add_rule(rule) 

318 

319 def remove_regex_rule(self, rule_id: str) -> None: 

320 self._regex.remove_rule(rule_id) 

321 

322 async def check_input(self, content: str) -> GuardResult: 

323 """Validate user input before agent processing.""" 

324 return await self._check(content, stage="input") 

325 

326 async def check_output(self, content: str) -> GuardResult: 

327 """Validate agent output before returning to user.""" 

328 return await self._check(content, stage="output") 

329 

330 async def check_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> GuardResult: 

331 """Validate tool calls for safety.""" 

332 content = f"Tool: {tool_name}\nArgs: {json.dumps(arguments)}" 

333 return await self._check(content, stage="tool_call") 

334 

335 async def _check(self, content: str, stage: str = "unknown") -> GuardResult: 

336 violations: List[GuardViolation] = [] 

337 

338 # Fast-path: regex scanning 

339 if self._enable_regex: 

340 violations.extend(self._regex.scan(content)) 

341 

342 # Deep check: LLM safety assessment 

343 if self._enable_safety and content.strip(): 

344 safety_violations = await self._safety.assess(content) 

345 violations.extend(safety_violations) 

346 

347 # Determine result 

348 if not violations: 

349 result = GuardResult(passed=True) 

350 else: 

351 redacted = self._regex.redact(content, violations) if any( 

352 v.action == GuardAction.REDACT for v in violations 

353 ) else None 

354 

355 result = GuardResult( 

356 passed=not any(v.action == GuardAction.BLOCK for v in violations), 

357 violations=violations, 

358 redacted_content=redacted, 

359 warnings=[v.message for v in violations if v.action == GuardAction.WARN], 

360 ) 

361 

362 self._audit_log.append(result) 

363 return result 

364 

365 def get_audit_log(self) -> List[Dict[str, Any]]: 

366 return [r.to_dict() for r in self._audit_log] 

367 

368 def get_statistics(self) -> Dict[str, int]: 

369 total = len(self._audit_log) 

370 blocked = sum(1 for r in self._audit_log if r.blocked) 

371 passed = sum(1 for r in self._audit_log if r.passed and not r.violations) 

372 warned = total - blocked - passed 

373 return { 

374 "total_checks": total, 

375 "passed": passed, 

376 "blocked": blocked, 

377 "warned": warned, 

378 } 

379 

380 

381# --------------------------------------------------------------------------- 

382# Exception 

383# --------------------------------------------------------------------------- 

384 

385class GuardViolationError(Exception): 

386 """Raised when guardrails block a request.""" 

387 

388 def __init__(self, result: GuardResult): 

389 self.result = result 

390 violations_summary = "; ".join( 

391 f"[{v.category.value}] {v.message}" for v in result.violations 

392 ) 

393 super().__init__(f"Guardrail blocked: {violations_summary}") 

394 

395 

396# --------------------------------------------------------------------------- 

397# Convenience: Pre-built Pipeline 

398# --------------------------------------------------------------------------- 

399 

400def create_default_pipeline() -> GuardrailsPipeline: 

401 """Create a GuardrailsPipeline with sensible defaults.""" 

402 return GuardrailsPipeline( 

403 regex_guard=RegexGuard(rules=DEFAULT_RULES), 

404 enable_regex=True, 

405 enable_safety=False, # LLM-based safety off by default; opt-in 

406 ) 

407 

408 

409def create_strict_pipeline() -> GuardrailsPipeline: 

410 """Create a GuardrailsPipeline with strict rules + LLM safety.""" 

411 return GuardrailsPipeline( 

412 regex_guard=RegexGuard(rules=DEFAULT_RULES), 

413 safety_guard=ContentSafetyGuard(), 

414 enable_regex=True, 

415 enable_safety=True, 

416 )