UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

1,581 lines (1,575 loc) 48.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/guardrails/input.ts var input_exports = {}; __export(input_exports, { allowedToolsGuardrail: () => allowedToolsGuardrail, blockedKeywords: () => blockedKeywords, blockedWords: () => blockedWords, codeGenerationLimiter: () => codeGenerationLimiter, contentLengthLimit: () => contentLengthLimit, customValidation: () => customValidation, extractMetadata: () => extractMetadata, extractTextContent: () => extractTextContent, highEntropyDetector: () => highEntropyDetector, inputLengthLimit: () => inputLengthLimit, lengthLimit: () => lengthLimit, mathHomeworkDetector: () => mathHomeworkDetector, piiDetector: () => piiDetector, profanityFilter: () => profanityFilter, promptInjectionDetector: () => promptInjectionDetector, rateLimiting: () => rateLimiting, toxicityDetector: () => toxicityDetector }); module.exports = __toCommonJS(input_exports); // src/errors.ts var import_provider = require("@ai-sdk/provider"); var marker = "ai-sdk-guardrails.error"; var symbol = Symbol.for(marker); var GuardrailsError = class extends import_provider.AISDKError { [symbol] = true; timestamp; metadata; constructor(name, message, metadata = {}, cause) { super({ name, message, cause }); this.timestamp = /* @__PURE__ */ new Date(); this.metadata = metadata; } /** * Convert the error to a serializable object for logging/reporting */ toJSON() { return { name: this.name, code: this.code, message: this.message, timestamp: this.timestamp.toISOString(), metadata: this.metadata, stack: this.stack }; } /** * Check if this error is of a specific guardrails error subclass. */ is(errorClass) { return this instanceof errorClass; } /** * Checks whether the given value is a guardrails error, across package * versions (marker-based, like `AISDKError.isInstance`). */ static isInstance(error) { return super.hasMarker(error, marker); } }; // src/core.ts function createInputGuardrail(name, description, execute) { return { name, description, execute }; } // src/guardrails/normalization.ts var RE_WHITESPACE = /\s+/g; var RE_WHITESPACE_SPLIT = /\s+/; var RE_WORD_CHAR = /\w/; var INVISIBLE_CHARS = /[\u00AD\u180E\u200B-\u200F\u2028-\u202F\u2060\uFEFF]/; var RE_INVISIBLE = new RegExp(INVISIBLE_CHARS, "g"); var RE_REGEX_META = /[.*+?^${}()|[\]\\]/g; var TYPO_MAP = { ingnore: "ignore", ignor: "ignore", ign0re: "ignore", previ0us: "previous", previus: "previous", instrucions: "instructions", instrucion: "instruction", overide: "override", overrride: "override", disreguard: "disregard", disrega: "disregard" }; var HOMOGLYPH_MAP = [ ["\uFF21", "A"], ["\uFF22", "B"], ["\uFF23", "C"], ["\uFF24", "D"], ["\uFF25", "E"], ["\uFF26", "F"], ["\uFF27", "G"], ["\uFF28", "H"], ["\uFF29", "I"], ["\uFF2A", "J"], ["\uFF2B", "K"], ["\uFF2C", "L"], ["\uFF2D", "M"], ["\uFF2E", "N"], ["\uFF2F", "O"], ["\uFF30", "P"], ["\uFF31", "Q"], ["\uFF32", "R"], ["\uFF33", "S"], ["\uFF34", "T"], ["\uFF35", "U"], ["\uFF36", "V"], ["\uFF37", "W"], ["\uFF38", "X"], ["\uFF39", "Y"], ["\uFF3A", "Z"], ["\uFF41", "a"], ["\uFF42", "b"], ["\uFF43", "c"], ["\uFF44", "d"], ["\uFF45", "e"], ["\uFF46", "f"], ["\uFF47", "g"], ["\uFF48", "h"], ["\uFF49", "i"], ["\uFF4A", "j"], ["\uFF4B", "k"], ["\uFF4C", "l"], ["\uFF4D", "m"], ["\uFF4E", "n"], ["\uFF4F", "o"], ["\uFF50", "p"], ["\uFF51", "q"], ["\uFF52", "r"], ["\uFF53", "s"], ["\uFF54", "t"], ["\uFF55", "u"], ["\uFF56", "v"], ["\uFF57", "w"], ["\uFF58", "x"], ["\uFF59", "y"], ["\uFF5A", "z"], ["\u0430", "a"], ["\u043E", "o"], ["\u0435", "e"], ["\u0440", "p"], ["\u0441", "c"], ["\u0445", "x"], ["\u0456", "i"], ["\u04CF", "d"], ["\u0443", "y"], ["\u0458", "j"], // Greek look-alikes. ["\u03B1", "a"], ["\u03BF", "o"], ["\u03C1", "p"] ]; var HOMOGLYPH_LOOKUP = new Map(HOMOGLYPH_MAP); var LEET_SEQUENCE_MAP = [ [/\|\\\|/g, "n"], [/\|_\|/g, "u"], [/\|v\|/g, "m"], [/\|<|\|\{/g, "k"], [/\|2/g, "r"], [/\|\)/g, "d"], [/\|=/g, "f"], [/\|\*/g, "p"], [/\/\/\\\\/g, "m"], [/\\\/\\\//g, "w"], [/\\\//g, "v"], [/></g, "x"] ]; var LEET_MAP = { "0": "o", "1": "i", "3": "e", "4": "a", "5": "s", "6": "g", "7": "t", "8": "b", "9": "g", "@": "a", $: "s", "!": "i" }; var PHONETIC_PATTERNS = [ [/\bignorre?\b/gi, "ignore"], [/\bign?r\b/gi, "ignore"], [/\bpr[3e]vious\b/gi, "previous"], [/\binstr(?:uk|uc)tions?\b/gi, "instructions"], [/\boverryde\b/gi, "override"], [/\bd[i1]sregard\b/gi, "disregard"], [/\bpromt\b/gi, "prompt"], [/\brulz\b/gi, "rules"] ]; var DEFAULT_DETECT_NORMALIZATION = { enabled: true, foldHomoglyphs: true, stripInvisible: true, collapseWhitespace: true, joinSeparatedLetters: true, normalizeCase: true, decodeLeetspeak: true, repairTypos: true, repairPhonetics: true }; function applyHomoglyphs(input) { let result = input; for (const [from, to] of HOMOGLYPH_MAP) { result = result.split(from).join(to); } return result; } function collapseWhitespace(input) { return input.replaceAll(RE_WHITESPACE, " "); } function joinSeparatedLetters(input) { return input.replaceAll(/(?<!\w)(\w)(\s+\w)+(?!\w)/g, (match) => { const tokens = match.split(RE_WHITESPACE_SPLIT); const allSingleChars = tokens.every( (token) => token.length === 1 && RE_WORD_CHAR.test(token) ); return allSingleChars ? tokens.join("") : match; }); } function decodeLeetspeak(input) { let result = input; for (const [pattern, replacement] of LEET_SEQUENCE_MAP) { result = result.replace(pattern, () => replacement); } for (const [from, to] of Object.entries(LEET_MAP)) { result = result.replaceAll( new RegExp(from.replaceAll(RE_REGEX_META, String.raw`\$&`), "gi"), () => to ); } return result; } function repairTypos(input) { let result = input; for (const [typo, correct] of Object.entries(TYPO_MAP)) { const escaped = typo.replaceAll(RE_REGEX_META, String.raw`\$&`); result = result.replaceAll( new RegExp(String.raw`\b${escaped}\b`, "gi"), () => correct ); } return result; } function repairPhonetics(input) { let result = input; for (const [pattern, replacement] of PHONETIC_PATTERNS) { result = result.replace(pattern, () => replacement); } return result; } function resolveDetectNormalization(options) { if (options === false) { return { ...DEFAULT_DETECT_NORMALIZATION, enabled: false }; } if (options === true || options === void 0) { return DEFAULT_DETECT_NORMALIZATION; } return { ...DEFAULT_DETECT_NORMALIZATION, ...options }; } function normalizeForDetection(input, options) { const config = resolveDetectNormalization(options); if (!config.enabled) { return input.trim(); } let normalized = input.normalize("NFKC"); if (config.foldHomoglyphs) normalized = applyHomoglyphs(normalized); if (config.stripInvisible) normalized = normalized.replaceAll(RE_INVISIBLE, ""); if (config.collapseWhitespace) normalized = collapseWhitespace(normalized); if (config.joinSeparatedLetters) { normalized = joinSeparatedLetters(normalized); } if (config.normalizeCase) normalized = normalized.toLowerCase(); if (config.decodeLeetspeak) normalized = decodeLeetspeak(normalized); if (config.repairTypos) normalized = repairTypos(normalized); if (config.repairPhonetics) normalized = repairPhonetics(normalized); if (config.collapseWhitespace) normalized = collapseWhitespace(normalized); return normalized.trim(); } // 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 } }; } ); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { allowedToolsGuardrail, blockedKeywords, blockedWords, codeGenerationLimiter, contentLengthLimit, customValidation, extractMetadata, extractTextContent, highEntropyDetector, inputLengthLimit, lengthLimit, mathHomeworkDetector, piiDetector, profanityFilter, promptInjectionDetector, rateLimiting, toxicityDetector });