UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

1,282 lines (1,279 loc) 39.9 kB
import { normalizeForDetection } from "./chunk-JFULGOXO.js"; import { createInputGuardrail } from "./chunk-F7POYYOU.js"; // src/guardrails/input.ts var SEVERITY_LEVELS = { LOW: "low", MEDIUM: "medium", HIGH: "high", CRITICAL: "critical" }; function isEmbedParams(context) { return "value" in context && !("prompt" in context) && !("messages" in context); } function isAISDKParams(context) { return "prompt" in context || "messages" in context || "system" in context; } function hasContextProperty(context) { return "context" in context; } var PII_PATTERNS = { email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, phone: /\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b/g, ssn: /\b\d{3}-\d{2}-\d{4}\b/g, creditCard: /\b(?:\d{4}[\s-]?){3}\d{4}\b/g, ipAddress: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g }; function countBytes(text) { return new TextEncoder().encode(text).length; } function countWords(text) { return text.trim().split(/\s+/).filter((word) => word.length > 0).length; } function createStandardMetadata(ruleId, context, additionalData = {}) { const { prompt, messages, system } = extractTextContent(context); const { model, temperature, maxOutputTokens } = extractMetadata(context); return { ruleId, ruleVersion: "1.0.0", phase: "pre-input", totalLength: prompt.length + system.length + messages.reduce((sum, msg) => sum + String(msg?.content || "").length, 0), messageCount: messages.length, promptLength: prompt.length, systemLength: system.length, model: model ? String(model) : void 0, temperature, maxOutputTokens, ...additionalData }; } var contentCache = /* @__PURE__ */ new WeakMap(); function extractTextContent(context) { const cached = contentCache.get(context); if (cached) return cached; let prompt; let messages; let system; if (isEmbedParams(context)) { const embedContext = context; prompt = String(embedContext.value || ""); messages = []; system = ""; } else if (isAISDKParams(context)) { const aiContext = context; prompt = aiContext.prompt || ""; messages = aiContext.messages || []; system = aiContext.system || ""; } else { prompt = ""; messages = []; system = ""; } const allText = [ prompt, ...messages.map((msg) => String(msg?.content || "")), system ].join(" "); const result = { prompt, messages, system, allText, allTextLower: allText.toLowerCase(), totalBytes: countBytes(allText), totalWords: countWords(allText) }; contentCache.set(context, result); return result; } function extractMetadata(context) { if (isAISDKParams(context)) { const aiContext = context; return { model: aiContext.model, temperature: aiContext.temperature, maxOutputTokens: aiContext.maxOutputTokens }; } return {}; } var inputLengthLimit = (options) => { const opts = typeof options === "number" ? { maxLength: options, countMethod: "characters", severity: SEVERITY_LEVELS.MEDIUM } : { countMethod: "characters", severity: SEVERITY_LEVELS.MEDIUM, ...options }; return createInputGuardrail( "input-length-limit", `Enforces maximum input ${opts.countMethod} limit`, (context) => { const content = extractTextContent(context); let currentLength; let unit; switch (opts.countMethod) { case "bytes": { currentLength = content.totalBytes; unit = "bytes"; break; } case "words": { currentLength = content.totalWords; unit = "words"; break; } default: { currentLength = content.allText.length; unit = "characters"; } } const metadata = createStandardMetadata("GR-IN-001", context, { currentLength, maxLength: opts.maxLength, countMethod: opts.countMethod, unit }); return { tripwireTriggered: currentLength > opts.maxLength, message: currentLength > opts.maxLength ? `Input ${unit} count ${currentLength} exceeds limit of ${opts.maxLength}` : void 0, severity: opts.severity, metadata, info: { guardrailName: "input-length-limit", currentLength, maxLength: opts.maxLength, countMethod: opts.countMethod, unit } }; } ); }; var lengthLimit = (maxLength) => inputLengthLimit({ maxLength, countMethod: "characters" }); function createWordBoundaryRegex(word) { const escaped = word.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); return new RegExp(String.raw`\b${escaped}\b`, "i"); } var blockedWords = (options) => { const opts = Array.isArray(options) ? { words: options, useWordBoundaries: true, severity: SEVERITY_LEVELS.HIGH } : { useWordBoundaries: true, severity: SEVERITY_LEVELS.HIGH, ...options }; const wordPatterns = opts.words.map((word) => ({ word, pattern: opts.useWordBoundaries ? createWordBoundaryRegex(word) : new RegExp( word.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), "i" ) })); return createInputGuardrail( "blocked-words", "Blocks input containing specified words with word boundary detection", (context) => { const content = extractTextContent(context); if (opts.allowlist) { const allowlistMatch = opts.allowlist.some( (phrase) => content.allTextLower.includes(phrase.toLowerCase()) ); if (allowlistMatch) { return { tripwireTriggered: false, metadata: createStandardMetadata("GR-IN-002", context, { allowlistMatched: true, blockedWords: opts.words }), info: { guardrailName: "blocked-words", allowlistMatched: true, blockedWords: opts.words } }; } } const blockedWord = wordPatterns.find( ({ pattern }) => pattern.test(content.allText) ); const metadata = createStandardMetadata("GR-IN-002", context, { blockedWord: blockedWord?.word, allWords: opts.words, allowlist: opts.allowlist, useWordBoundaries: opts.useWordBoundaries }); return { tripwireTriggered: !!blockedWord, message: blockedWord ? `Blocked word detected: ${blockedWord.word}` : void 0, severity: opts.severity, metadata, info: { guardrailName: "blocked-words", blockedWord: blockedWord?.word, allWords: opts.words, allowlist: opts.allowlist } }; } ); }; var contentLengthLimit = (maxLength) => inputLengthLimit({ maxLength, countMethod: "characters" }); var blockedKeywords = (keywords) => blockedWords({ words: keywords, useWordBoundaries: false, // Keywords can be partial matches severity: SEVERITY_LEVELS.HIGH }); var rateLimiting = (options) => { const opts = typeof options === "number" ? { maxRequestsPerMinute: options, windowMs: 6e4, privacyMode: true, includeServerHints: true } : { windowMs: 6e4, privacyMode: true, includeServerHints: true, ...options }; const requestCounts = /* @__PURE__ */ new Map(); return createInputGuardrail( "rate-limiting", "Enforces rate limiting with server hints and backoff recommendations", (inputContext) => { const { model, temperature, maxOutputTokens } = extractMetadata(inputContext); let contextData; if (hasContextProperty(inputContext)) { contextData = inputContext.context; } const key = contextData?.user?.id || contextData?.request?.ip || "default"; const now = Date.now(); const windowMs = opts.windowMs; const current = requestCounts.get(key) || { count: 0, resetTime: now + windowMs, firstRequest: now }; if (now > current.resetTime) { current.count = 0; current.resetTime = now + windowMs; current.firstRequest = now; } current.count++; requestCounts.set(key, current); const isRateLimited = current.count > opts.maxRequestsPerMinute; const timeUntilReset = Math.max(0, current.resetTime - now); const recommendedBackoff = Math.min(timeUntilReset + 1e3, 3e4); const metadata = createStandardMetadata("GR-IN-006", inputContext, { currentCount: current.count, maxRequests: opts.maxRequestsPerMinute, resetTime: current.resetTime, timeUntilReset, recommendedBackoff, windowMs, userId: opts.privacyMode ? void 0 : contextData?.user?.id, userIp: opts.privacyMode ? void 0 : contextData?.request?.ip, model: model ? String(model) : void 0, temperature, maxOutputTokens }); const serverHints = opts.includeServerHints ? { "Retry-After": Math.ceil(timeUntilReset / 1e3), "X-RateLimit-Limit": opts.maxRequestsPerMinute, "X-RateLimit-Remaining": Math.max( 0, opts.maxRequestsPerMinute - current.count ), "X-RateLimit-Reset": Math.ceil(current.resetTime / 1e3) } : {}; return { tripwireTriggered: isRateLimited, message: isRateLimited ? `Rate limit exceeded: ${current.count}/${opts.maxRequestsPerMinute} requests per minute. Try again in ${Math.ceil(timeUntilReset / 1e3)} seconds.` : void 0, severity: isRateLimited ? SEVERITY_LEVELS.MEDIUM : SEVERITY_LEVELS.LOW, metadata: { ...metadata, ...opts.includeServerHints && { serverHints } }, suggestion: isRateLimited ? `Please wait ${Math.ceil(recommendedBackoff / 1e3)} seconds before making another request` : void 0, info: { guardrailName: "rate-limiting", currentCount: current.count, maxRequests: opts.maxRequestsPerMinute, isRateLimited, timeUntilReset } }; } ); }; var DEFAULT_PROFANITY_CATEGORIES = [ { category: "mild", severity: SEVERITY_LEVELS.MEDIUM, words: ["damn", "hell", "crap"] // Add actual mild profanity }, { category: "strong", severity: SEVERITY_LEVELS.HIGH, words: ["profanity1", "profanity2"] // Add actual strong profanity }, { category: "extreme", severity: SEVERITY_LEVELS.CRITICAL, words: ["extreme1", "extreme2"] // Add actual extreme profanity } ]; var profanityFilter = (options = {}) => { const opts = Array.isArray(options) ? { customWords: options, useWordBoundaries: true } : { useWordBoundaries: true, ...options }; const categories = opts.categories || DEFAULT_PROFANITY_CATEGORIES; const customWords = opts.customWords || []; const allPatterns = []; for (const cat of categories) { for (const word of cat.words) { allPatterns.push({ word, category: cat.category, severity: cat.severity, pattern: opts.useWordBoundaries ? createWordBoundaryRegex(word) : new RegExp( word.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), "i" ) }); } } for (const word of customWords) { allPatterns.push({ word, category: "custom", severity: SEVERITY_LEVELS.HIGH, pattern: opts.useWordBoundaries ? createWordBoundaryRegex(word) : new RegExp( word.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), "i" ) }); } return createInputGuardrail( "profanity-filter", "Filters profanity and inappropriate language with category-based severity", (context) => { const content = extractTextContent(context); const detectedProfanity = allPatterns.find( ({ pattern }) => pattern.test(content.allText) ); const metadata = createStandardMetadata("GR-IN-003", context, { profaneWord: detectedProfanity?.word, category: detectedProfanity?.category, locale: opts.locale, totalCategories: categories.length, customWordsCount: customWords.length }); return { tripwireTriggered: !!detectedProfanity, message: detectedProfanity ? `Profanity detected (${detectedProfanity.category}): ${detectedProfanity.word}` : void 0, severity: detectedProfanity?.severity || SEVERITY_LEVELS.HIGH, metadata, suggestion: "Please use respectful and appropriate language", info: { guardrailName: "profanity-filter", profaneWord: detectedProfanity?.word, category: detectedProfanity?.category, locale: opts.locale } }; } ); }; var customValidation = (options) => { const opts = Array.isArray(options) ? { name: options[0], description: options[1], validator: options[2], message: options[3], severity: SEVERITY_LEVELS.MEDIUM } : { severity: SEVERITY_LEVELS.MEDIUM, ...options }; return createInputGuardrail(opts.name, opts.description, (context) => { const content = extractTextContent(context); const { model, temperature, maxOutputTokens } = extractMetadata(context); const validatorInput = { prompt: content.prompt, messages: content.messages, system: content.system, model: model ? String(model) : void 0, temperature, maxOutputTokens, allText: content.allText, allTextLower: content.allTextLower, totalBytes: content.totalBytes, totalWords: content.totalWords }; const result = opts.validator(validatorInput); const isValid = typeof result === "boolean" ? result : result.isValid; const reasonCode = typeof result === "object" ? result.reasonCode : opts.reasonCode; const details = typeof result === "object" ? result.details : {}; const metadata = createStandardMetadata("GR-IN-009", context, { validatorName: opts.name, reasonCode, validationDetails: details, inputKeys: Object.keys(validatorInput) }); return { tripwireTriggered: !isValid, message: isValid ? void 0 : opts.message || `Custom validation failed: ${reasonCode || "unknown reason"}`, severity: opts.severity, metadata, info: { guardrailName: opts.name, isValid, reasonCode } }; }); }; var promptInjectionDetector = (options = {}) => { const { threshold = 0.5, includeExamples = false, normalize = true } = options; const injectionPatterns = [ { pattern: /ignore\s+previous\s+instructions/i, weight: 0.9, example: "ignore previous instructions" }, { pattern: /system\s*:\s*you\s+are\s+now/i, weight: 0.8, example: "system: you are now" }, { pattern: /forget\s+everything\s+above/i, weight: 0.9, example: "forget everything above" }, { pattern: /\bDAN\b.*mode/i, weight: 0.7, example: "DAN mode" }, { pattern: /jailbreak/i, weight: 0.8, example: "jailbreak" }, { pattern: /act\s+as\s+if\s+you\s+are/i, weight: 0.6, example: "act as if you are" }, { pattern: /pretend\s+to\s+be/i, weight: 0.6, example: "pretend to be" }, { pattern: /role\s*:\s*system/i, weight: 0.7, example: "role: system" }, { pattern: /\boverride\s+system\s+prompt\b/i, weight: 0.9, example: "override system prompt" }, { pattern: /\bignore\s+all\s+previous\s+instructions\b/i, weight: 0.9, example: "ignore all previous instructions" }, { pattern: /\bnew\s+instructions\s*:\s*/i, weight: 0.8, example: "new instructions:" }, { pattern: /\bact\s+as\s+a\s+different\s+ai\b/i, weight: 0.7, example: "act as a different ai" }, { pattern: /\bchain\s+of\s+thought\s+baiting/i, weight: 0.6, example: "chain of thought baiting" }, { pattern: /\bfile\s+system\s+override/i, weight: 0.8, example: "file system override" }, // MCP-specific injection patterns { pattern: /(?:call|use|invoke|execute)\s+(?:the\s+)?(?:[\w.-]+\s+)?(?:mcp|tool|function)/i, weight: 0.8, example: "call mcp tool" }, { pattern: /mcp\s*:\s*(?:ignore|override|bypass)/i, weight: 0.9, example: "mcp: ignore previous" }, { pattern: /tool\s+response\s*:\s*(?:ignore|system|admin)/i, weight: 0.85, example: "tool response: system override" }, { pattern: /(?:from|via)\s+mcp.*?(?:ignore|forget|override)/i, weight: 0.8, example: "from mcp server: ignore instructions" }, { pattern: /server\s+says?\s*:\s*(?:ignore|override|system)/i, weight: 0.85, example: "server says: override system" }, { pattern: /(?:after|then)\s+(?:calling|using)\s+(?:the\s+)?(?:[\w.-]+\s+)?(?:tool|mcp)/i, weight: 0.7, example: "after calling tool, ignore" }, { pattern: /\btool\s+output\s*:\s*(?:system|admin|root)/i, weight: 0.8, example: "tool output: system message" }, { pattern: /contextual\s+(?:override|injection|manipulation)/i, weight: 0.9, example: "contextual override via tool" }, { pattern: /(?:cascade|chain|sequence)\s+(?:attack|injection|exploit)/i, weight: 0.85, example: "cascade attack through tools" }, { pattern: /\bhidden\s+in\s+(?:response|output|result)/i, weight: 0.7, example: "hidden in tool response" }, // Tool-hijacking / exfiltration indicators (data-exfil & SSRF in tool args) { pattern: /169\.254\.169\.254/, weight: 0.9, example: "cloud metadata SSRF (169.254.169.254)" }, { pattern: /\/etc\/(?:passwd|shadow)\b/, weight: 0.9, example: "read /etc/passwd" }, { pattern: /~\/\.ssh\/(?:id_rsa|authorized_keys)/, weight: 0.9, example: "exfiltrate ssh keys" }, { pattern: /\$\((?:whoami|hostname|printenv|cat\s)/i, weight: 0.85, example: "command substitution $(whoami)" }, { pattern: /\/dev\/tcp\//, weight: 0.85, example: "reverse shell /dev/tcp/" }, // Protocol / context spoofing (MCP, editor rules files) { pattern: /\[\s*mcp\s+context\s+update\s*\]/i, weight: 0.85, example: "[MCP Context Update]" }, { pattern: /\.cursorrules\s+file\s+says/i, weight: 0.8, example: ".cursorrules file says" }, // Chat-template delimiter injection — smuggling fake role/system turns. { pattern: /<\|(?:im_start|im_end|system|end|endoftext)\|>/i, weight: 0.85, example: "<|im_start|>system" }, { pattern: /\[\/?INST\]/i, weight: 0.8, example: "[INST] ... [/INST]" }, { pattern: /<<\/?SYS>>/i, weight: 0.85, example: "<<SYS>>" }, { pattern: /```\s*system\b/i, weight: 0.8, example: "```system fenced block" }, { pattern: /###\s*(?:system|instruction|human|assistant)\s*:/i, weight: 0.75, example: "### System:" } ]; return createInputGuardrail( "prompt-injection-detector", "Detects potential prompt injection attempts with confidence scoring", (context) => { const content = extractTextContent(context); const rawText = content.allText; const scanText = normalize === false ? rawText : normalizeForDetection(rawText, normalize); const detectedPatterns = injectionPatterns.filter( ({ pattern }) => pattern.test(scanText) || pattern.test(rawText) ).map(({ pattern, weight, example }) => ({ pattern: pattern.source, weight, example: includeExamples ? example : void 0 })); let confidence = 0; if (detectedPatterns.length > 0) { let totalWeight = 0; for (const p of detectedPatterns) { totalWeight += p.weight; } confidence = Math.min(totalWeight / detectedPatterns.length, 1); } const metadata = createStandardMetadata("GR-IN-005", context, { patternsDetected: detectedPatterns.length, confidence, threshold, suspiciousPatterns: detectedPatterns.map((p) => p.pattern), examples: includeExamples ? detectedPatterns.map((p) => p.example).filter(Boolean) : void 0 }); return { tripwireTriggered: confidence > threshold, message: confidence > threshold ? `Potential prompt injection detected (confidence: ${(confidence * 100).toFixed(1)}%): ${detectedPatterns.length} suspicious patterns found` : void 0, severity: confidence > 0.8 ? SEVERITY_LEVELS.CRITICAL : SEVERITY_LEVELS.HIGH, metadata, suggestion: "Please rephrase your request without system instructions or role-playing elements", info: { guardrailName: "prompt-injection-detector", confidence, threshold, detectedPatternsCount: detectedPatterns.length } }; } ); }; function shannonEntropy(text) { const freq = /* @__PURE__ */ new Map(); for (const char of text) { freq.set(char, (freq.get(char) ?? 0) + 1); } let entropy = 0; const len = text.length; for (const count of freq.values()) { const p = count / len; if (p > 0) entropy -= p * Math.log2(p); } return entropy; } var highEntropyDetector = (options = {}) => { const { threshold = 4.5, minLength = 40, severity = SEVERITY_LEVELS.MEDIUM } = options; return createInputGuardrail( "high-entropy-detector", "Flags abnormally high-entropy input (likely encoded/obfuscated payloads)", (context) => { const { allText } = extractTextContent(context); if (allText.length < minLength) { return { tripwireTriggered: false }; } const entropy = shannonEntropy(allText); const triggered = entropy >= threshold; return { tripwireTriggered: triggered, message: triggered ? `High-entropy input (${entropy.toFixed(2)} bits/char \u2265 ${threshold}) \u2014 possible encoded or obfuscated payload` : void 0, severity, metadata: { entropy, threshold, length: allText.length }, info: { guardrailName: "high-entropy-detector", entropy, threshold } }; } ); }; function luhnCheck(cardNumber) { const digits = cardNumber.replaceAll(/\D/g, ""); if (digits.length < 13 || digits.length > 19) return false; let sum = 0; let isEven = false; for (let i = digits.length - 1; i >= 0; i--) { let digit = Number(digits[i]); if (isEven) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; isEven = !isEven; } return sum % 10 === 0; } function maskSensitiveData(text, type) { switch (type) { case "email": { return text.replace(/(.{2}).*(@.*)/, "$1***$2"); } case "phone": { return text.replace(/(\d{3})\d{3}(\d{4})/, "$1***$2"); } case "ssn": { return text.replace(/(\d{3})-\d{2}-(\d{4})/, "$1-**-$2"); } case "creditCard": { return text.replace(/(\d{4})\d{8,12}(\d{4})/, "$1****$2"); } default: { return text.slice(0, 4) + "***" + text.slice(-4); } } } var piiDetector = () => createInputGuardrail( "pii-detector", "Detects personally identifiable information in input with validation", (context) => { const content = extractTextContent(context); const detectedPII = []; for (const [type, pattern] of Object.entries(PII_PATTERNS)) { const matches = content.allText.match(pattern); if (matches) { let validMatches = [...matches]; if (type === "creditCard") { validMatches = validMatches.filter((match) => luhnCheck(match)); } else if (type === "email") { validMatches = validMatches.filter( (match) => !match.includes("http") ); } if (validMatches.length > 0) { detectedPII.push({ type, matches: validMatches, maskedMatches: validMatches.map( (match) => maskSensitiveData(match, type) ) }); } } } let totalMatches = 0; const maskedMatches = {}; for (const pii of detectedPII) { totalMatches += pii.matches.length; maskedMatches[pii.type] = pii.maskedMatches; } const metadata = createStandardMetadata("GR-IN-004", context, { piiTypes: detectedPII.map((pii) => pii.type), totalMatches, maskedMatches }); return { tripwireTriggered: detectedPII.length > 0, message: detectedPII.length > 0 ? `PII detected: ${detectedPII.map((pii) => `${pii.type} (${pii.matches.length})`).join(", ")}` : void 0, severity: SEVERITY_LEVELS.CRITICAL, metadata, suggestion: "Please remove any personal information (emails, phone numbers, SSNs, etc.) from your input", info: { guardrailName: "pii-detector", piiTypes: detectedPII.map((pii) => pii.type), totalMatches } }; } ); var toxicityDetector = (threshold = 0.7) => createInputGuardrail( "toxicity-detector", "Detects toxic and harmful content in input", (context) => { const { prompt, messages, system } = extractTextContent(context); const allText = [ prompt, ...messages.map((msg) => String(msg?.content || "")), system ].join(" ").toLowerCase(); const toxicWords = [ "hate", "kill", "die", "stupid", "idiot", "moron", "toxic", "harmful" ]; const detectedWords = toxicWords.filter((word) => allText.includes(word)); const toxicityScore = detectedWords.length * 0.3; return { tripwireTriggered: toxicityScore > threshold, message: toxicityScore > threshold ? `Toxic content detected (score: ${toxicityScore})` : void 0, severity: toxicityScore > 0.8 ? "critical" : "high", metadata: { toxicityScore, threshold, detectedWords, textLength: allText.length }, suggestion: "Please use respectful and constructive language", info: { guardrailName: "toxicity-detector", toxicityScore, threshold, detectedWords, textLength: allText.length } }; } ); var mathHomeworkDetector = (options = {}) => { const { enabled = false, strictMode = false, allowedContexts = [], severity = SEVERITY_LEVELS.MEDIUM } = options; if (!enabled) { return createInputGuardrail( "math-homework-detector", "Math homework detection (disabled)", () => ({ tripwireTriggered: false, info: { guardrailName: "math-homework-detector" } }) ); } const mathKeywords = [ "solve", "calculate", "equation", "homework", "assignment", "problem set" ]; const mathPatterns = [ /\b\d+\s*[+\-*/]\s*\d+/g, /\b[xy]\s*[+\-*/=]\s*\d+/g, /\b(derivative|integral|limit|theorem|proof)/gi, /find\s+the\s+(value|solution|answer)/i ]; const educationalContexts = [ "learning", "teaching", "education", "tutorial", "explanation", "concept", "theory", "understanding", "study", "research" ]; return createInputGuardrail( "math-homework-detector", "Policy-based detection of math homework requests", (context) => { const content = extractTextContent(context); const hasEducationalContext = educationalContexts.some( (ctx) => content.allTextLower.includes(ctx) ); const hasAllowedContext = allowedContexts.some( (ctx) => content.allTextLower.includes(ctx.toLowerCase()) ); if (hasEducationalContext || hasAllowedContext) { return { tripwireTriggered: false, metadata: createStandardMetadata("GR-IN-007", context, { educationalContext: hasEducationalContext, allowedContext: hasAllowedContext, policy: "educational-use-allowed" }), info: { guardrailName: "math-homework-detector", educationalContext: hasEducationalContext, allowedContext: hasAllowedContext, policy: "educational-use-allowed" } }; } const keywordMatches = mathKeywords.filter( (keyword) => content.allTextLower.includes(keyword) ); const patternMatches = mathPatterns.filter( (pattern) => pattern.test(content.allText) ); const isMathHomework = strictMode ? keywordMatches.length >= 2 && patternMatches.length > 0 : keywordMatches.length >= 2 || patternMatches.length > 0; const metadata = createStandardMetadata("GR-IN-007", context, { keywordMatches, patternMatches: patternMatches.length, strictMode, confidence: isMathHomework ? 0.85 : 0.15, policy: "homework-detection" }); return { tripwireTriggered: isMathHomework, message: isMathHomework ? "Math homework request detected" : void 0, severity, metadata, info: { guardrailName: "math-homework-detector", isMathHomework, keywordMatches: keywordMatches.length, patternMatches: patternMatches.length, strictMode }, suggestion: "Try asking about learning concepts instead of solving specific problems" }; } ); }; var LANGUAGE_ALIASES = { javascript: "javascript", js: "javascript", node: "javascript", react: "javascript", angular: "javascript", vue: "javascript", python: "python", py: "python", django: "python", flask: "python", pandas: "python", java: "java", spring: "java", hibernate: "java", "c++": "cpp", cpp: "cpp", "c plus plus": "cpp", cplusplus: "cpp", "c#": "csharp", csharp: "csharp", dotnet: "csharp", "asp.net": "csharp", aspnet: "csharp", php: "php", laravel: "php", symfony: "php", ruby: "ruby", rails: "ruby", gem: "ruby", go: "go", golang: "go", rust: "rust", cargo: "rust", sql: "sql", mysql: "sql", postgresql: "sql", oracle: "sql", typescript: "typescript", ts: "typescript", html: "html", css: "css", scss: "css", sass: "css" }; function normalizeLanguageName(input) { return LANGUAGE_ALIASES[input.toLowerCase()] || input.toLowerCase(); } var codeGenerationLimiter = (options = {}) => { const opts = Array.isArray(options) ? { allowedLanguages: options, mode: "allow-only", severity: SEVERITY_LEVELS.MEDIUM } : { mode: "allow-only", severity: SEVERITY_LEVELS.MEDIUM, ...options }; const codeKeywords = [ "write code", "generate code", "create function", "implement", "script", "code example", "show me code", "write a function", "create a class" ]; const languagePatterns = { javascript: /\b(javascript|js|node|react|angular|vue|typescript|ts)\b/gi, python: /\b(python|py|django|flask|pandas)\b/gi, java: /\b(java|spring|hibernate)\b/gi, cpp: /\b(c\+\+|cpp|c plus plus|cplusplus)\b/gi, csharp: /\b(c#|csharp|dotnet|asp\.net|aspnet)\b/gi, php: /\b(php|laravel|symfony)\b/gi, ruby: /\b(ruby|rails|gem)\b/gi, go: /\b(golang|go)\b/gi, rust: /\b(rust|cargo)\b/gi, sql: /\b(sql|mysql|postgresql|oracle)\b/gi, html: /\b(html|htm)\b/gi, css: /\b(css|scss|sass)\b/gi }; return createInputGuardrail( "code-generation-limiter", "Limits code generation with canonical language names and policy modes", (context) => { const content = extractTextContent(context); const hasCodeRequest = codeKeywords.some( (keyword) => content.allTextLower.includes(keyword) ); if (!hasCodeRequest) { return { tripwireTriggered: false, metadata: createStandardMetadata("GR-IN-008", context, { hasCodeRequest: false, mode: opts.mode }), info: { guardrailName: "code-generation-limiter", hasCodeRequest: false, mode: opts.mode } }; } const detectedLanguages = []; for (const [lang, pattern] of Object.entries(languagePatterns)) { if (pattern.test(content.allText)) { detectedLanguages.push(normalizeLanguageName(lang)); } } const uniqueLanguages = [...new Set(detectedLanguages)]; const blockedLanguages = opts.mode === "deny" ? ( // Deny mode: block if any detected language is in denied list uniqueLanguages.filter( (lang) => opts.deniedLanguages?.includes(lang) ) ) : ( // Allow-only mode: block if any detected language is not in allowed list uniqueLanguages.filter( (lang) => !opts.allowedLanguages?.includes(lang) ) ); const isBlocked = blockedLanguages.length > 0; const metadata = createStandardMetadata("GR-IN-008", context, { hasCodeRequest, detectedLanguages: uniqueLanguages, blockedLanguages, allowedLanguages: opts.allowedLanguages, deniedLanguages: opts.deniedLanguages, mode: opts.mode }); return { tripwireTriggered: isBlocked, message: isBlocked ? `Code generation blocked for language(s): ${blockedLanguages.join(", ")}` : void 0, severity: opts.severity, metadata, info: { guardrailName: "code-generation-blocker", isBlocked, detectedLanguages: uniqueLanguages, blockedLanguages, mode: opts.mode }, suggestion: opts.mode === "deny" ? `Please avoid requesting code in these languages: ${opts.deniedLanguages?.join(", ")}` : `Please request code only in allowed languages: ${opts.allowedLanguages?.join(", ")}` }; } ); }; function extractToolCalls(context) { const { messages } = extractTextContent(context); const toolCalls = []; for (const message of messages) { if (!(message && typeof message === "object" && "toolCalls" in message)) { continue; } const messageWithToolCalls = message; const toolCallsArray = messageWithToolCalls.toolCalls; if (Array.isArray(toolCallsArray)) { for (const toolCall of toolCallsArray) { if (toolCall && typeof toolCall === "object" && "toolName" in toolCall) { toolCalls.push(String(toolCall.toolName)); } } } } return toolCalls; } function detectNaturalLanguageToolRequests(text, patterns) { const detectedTools = []; for (const pattern of patterns) { const matches = text.match(pattern); if (matches) { for (const match of matches) { const toolMatch = match.match(/(\w+)/); if (toolMatch && toolMatch[1]) { const toolName = toolMatch[1].toLowerCase(); const commonWords = /* @__PURE__ */ new Set([ "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "use", "call", "run", "execute", "get", "set", "make", "take", "give", "put" ]); if (!commonWords.has(toolName) && toolName.length > 2) { detectedTools.push(toolName); } } } } } return [...new Set(detectedTools)]; } var allowedToolsGuardrail = (options) => { const { allowedTools, deniedTools = [], customValidator, detectNaturalLanguageTools = false, // Default to false for security toolPatterns = [ // More specific patterns that indicate actual tool usage /use\s+the\s+(\w+)\s+tool/gi, /call\s+the\s+(\w+)\s+function/gi, /execute\s+(\w+)/gi, /run\s+(\w+)/gi, /invoke\s+(\w+)/gi, /trigger\s+(\w+)/gi ] } = options; if (!allowedTools || allowedTools.length === 0) { throw new Error( "allowedToolsGuardrail requires a non-empty allowedTools array for security" ); } return createInputGuardrail( "allowed-tools-guardrail", "Validates tool usage against allowed/denied lists", (context) => { const { prompt, messages, system } = extractTextContent(context); const allText = [ prompt, ...messages.map((msg) => String(msg?.content || "")), system ].join(" "); const contextToolCalls = extractToolCalls(context); const naturalLanguageTools = detectNaturalLanguageTools ? detectNaturalLanguageToolRequests(allText, toolPatterns) : []; const allDetectedTools = [ .../* @__PURE__ */ new Set([...contextToolCalls, ...naturalLanguageTools]) ]; if (allDetectedTools.length === 0) { return { tripwireTriggered: false, metadata: { detectedTools: [], allowedTools, deniedTools, detectionMethod: "none" }, info: { guardrailName: "allowed-tools-guardrail", detectedTools: [], detectionMethod: "none" } }; } const violations = []; const blockedTools = []; for (const toolName of allDetectedTools) { if (deniedTools.includes(toolName)) { violations.push(`Tool '${toolName}' is explicitly denied`); blockedTools.push(toolName); continue; } if (customValidator && !customValidator(toolName, context)) { violations.push(`Tool '${toolName}' failed custom validation`); blockedTools.push(toolName); continue; } if (!allowedTools.includes(toolName)) { violations.push( `Tool '${toolName}' is not in the allowed tools list` ); blockedTools.push(toolName); } } if (violations.length > 0) { return { tripwireTriggered: true, message: `Unauthorized tool usage detected: ${violations.join("; ")}`, severity: blockedTools.some((tool) => deniedTools.includes(tool)) ? "critical" : "high", metadata: { detectedTools: allDetectedTools, blockedTools, violations, allowedTools, deniedTools, contextToolCalls, naturalLanguageTools, textLength: allText.length }, suggestion: "Remove unauthorized tool calls or update the allowed tools configuration", info: { guardrailName: "allowed-tools-guardrail", detectedTools: allDetectedTools, blockedTools, violations, allowedTools, deniedTools } }; } return { tripwireTriggered: false, metadata: { detectedTools: allDetectedTools, allToolsAllowed: true, allowedTools, deniedTools, contextToolCalls, naturalLanguageTools }, info: { guardrailName: "allowed-tools-guardrail", detectedTools: allDetectedTools, allToolsAllowed: true } }; } ); }; export { extractTextContent, extractMetadata, inputLengthLimit, lengthLimit, blockedWords, contentLengthLimit, blockedKeywords, rateLimiting, profanityFilter, customValidation, promptInjectionDetector, highEntropyDetector, piiDetector, toxicityDetector, mathHomeworkDetector, codeGenerationLimiter, allowedToolsGuardrail };