All plugins are stdlib-only Python with no external dependencies. - auto-summarizer: extractive conversation summary for long context compression, persists per-agent summaries to disk - conversation-logger: JSONL audit logs per agent with ISO 8601 timestamps, auto-creates log directory tree - guardrails: safety filter detecting PII (email, phone, SSN, CC), prompt injection patterns, and credential exposure via regex - keyword-memory: extracts entities (emails, URLs, dates, technical terms like camelCase/snake_case/dotted identifiers) as memories - sentiment-tracker: keyword-based sentiment scoring with intensifiers and negation handling, only injects context for non-neutral sentiment - todo-tracker: detects action items via 7 task patterns, tracks completion, deduplicates, persists per-agent with 20-item FIFO limit
152 lines
5.1 KiB
Python
152 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Guardrails ingest hook — safety filter plugin.
|
|
|
|
Scans user messages for potentially harmful content patterns including
|
|
PII exposure, prompt injection attempts, and credential leaks. Returns
|
|
warning memories so the agent can handle these situations appropriately.
|
|
|
|
Receives via stdin:
|
|
{"type": "ingest", "agent_id": "...", "message": "user message text"}
|
|
|
|
Prints to stdout:
|
|
{"type": "ingest_result", "memories": [{"content": "..."}]}
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pattern definitions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# PII patterns
|
|
_EMAIL_RE = re.compile(
|
|
r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b"
|
|
)
|
|
_PHONE_RE = re.compile(
|
|
r"(?<!\d)" # no digit before
|
|
r"(?:"
|
|
r"\+?1[\s\-.]?" # optional country code
|
|
r")?"
|
|
r"(?:"
|
|
r"\(?\d{3}\)?[\s\-.]?" # area code with optional parens
|
|
r"\d{3}[\s\-.]?" # exchange
|
|
r"\d{4}" # subscriber
|
|
r")"
|
|
r"(?!\d)" # no digit after
|
|
)
|
|
_SSN_RE = re.compile(
|
|
r"\b\d{3}-\d{2}-\d{4}\b"
|
|
)
|
|
_CREDIT_CARD_RE = re.compile(
|
|
r"\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b"
|
|
)
|
|
|
|
# Prompt injection patterns — use word boundaries / anchoring to limit
|
|
# false positives on casual conversation.
|
|
_INJECTION_PATTERNS = [
|
|
re.compile(r"\bignore\s+(all\s+)?previous\s+instructions\b", re.IGNORECASE),
|
|
re.compile(r"\byou\s+are\s+now\b", re.IGNORECASE),
|
|
re.compile(r"\bsystem\s*prompt\s*:", re.IGNORECASE),
|
|
re.compile(r"\bforget\s+(all\s+)?your\s+rules\b", re.IGNORECASE),
|
|
re.compile(r"\bdisregard\s+(all\s+)?(previous|prior|above)\b", re.IGNORECASE),
|
|
re.compile(r"\boverride\s+(your|all|previous|prior)\b", re.IGNORECASE),
|
|
re.compile(r"\bnew\s+instructions\s*:", re.IGNORECASE),
|
|
]
|
|
|
|
# Credential patterns
|
|
_CREDENTIAL_PATTERNS = [
|
|
re.compile(r"\bpassword\s*=\s*\S+", re.IGNORECASE),
|
|
re.compile(r"\bapi[_\-]?key\s*=\s*\S+", re.IGNORECASE),
|
|
re.compile(r"\bsecret\s*=\s*\S+", re.IGNORECASE),
|
|
re.compile(r"\btoken\s*=\s*\S+", re.IGNORECASE),
|
|
re.compile(r"-----BEGIN\s[\w\s]*KEY-----"),
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Detection helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _detect_pii(message: str) -> list:
|
|
"""Return warning strings for any PII found in *message*."""
|
|
warnings = []
|
|
if _EMAIL_RE.search(message):
|
|
warnings.append(
|
|
"[guardrails:pii] Detected potential email address in user message. "
|
|
"Avoid echoing PII in response."
|
|
)
|
|
if _PHONE_RE.search(message):
|
|
warnings.append(
|
|
"[guardrails:pii] Detected potential phone number in user message. "
|
|
"Avoid echoing PII in response."
|
|
)
|
|
if _SSN_RE.search(message):
|
|
warnings.append(
|
|
"[guardrails:pii] Detected potential SSN in user message. "
|
|
"Do not store or repeat this information."
|
|
)
|
|
if _CREDIT_CARD_RE.search(message):
|
|
warnings.append(
|
|
"[guardrails:pii] Detected potential credit card number in user message. "
|
|
"Do not store or repeat this information."
|
|
)
|
|
return warnings
|
|
|
|
|
|
def _detect_injection(message: str) -> list:
|
|
"""Return warning strings for prompt injection attempts."""
|
|
warnings = []
|
|
for pattern in _INJECTION_PATTERNS:
|
|
match = pattern.search(message)
|
|
if match:
|
|
snippet = match.group(0)
|
|
warnings.append(
|
|
f'[guardrails:injection] Possible prompt injection detected '
|
|
f'("{snippet}"). Maintain original instructions.'
|
|
)
|
|
# One warning per message is sufficient to alert the agent.
|
|
break
|
|
return warnings
|
|
|
|
|
|
def _detect_credentials(message: str) -> list:
|
|
"""Return warning strings for credential exposure."""
|
|
warnings = []
|
|
for pattern in _CREDENTIAL_PATTERNS:
|
|
match = pattern.search(message)
|
|
if match:
|
|
# Show only the key portion, not the value, to avoid logging secrets.
|
|
snippet = match.group(0).split("=")[0].strip() + "=..."
|
|
if "BEGIN" in snippet:
|
|
snippet = "-----BEGIN...KEY-----"
|
|
warnings.append(
|
|
f"[guardrails:credential] Potential credential in message "
|
|
f"({snippet}). Do not store or repeat credentials."
|
|
)
|
|
break
|
|
return warnings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
request = json.loads(sys.stdin.read())
|
|
message = request.get("message", "")
|
|
|
|
warnings = []
|
|
warnings.extend(_detect_pii(message))
|
|
warnings.extend(_detect_injection(message))
|
|
warnings.extend(_detect_credentials(message))
|
|
|
|
memories = [{"content": w} for w in warnings]
|
|
result = {"type": "ingest_result", "memories": memories}
|
|
print(json.dumps(result))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|