ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
1,282 lines (1,279 loc) • 39.9 kB
JavaScript
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
};