ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
909 lines (902 loc) • 29.5 kB
JavaScript
;
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/tools.ts
var tools_exports = {};
__export(tools_exports, {
expectedToolUse: () => expectedToolUse,
extractToolNamesFromResult: () => extractToolNamesFromResult,
toolEgressPolicy: () => toolEgressPolicy
});
module.exports = __toCommonJS(tools_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 createOutputGuardrail(name, execute) {
return { name, execute };
}
// src/guardrails/output.ts
var EMPTY_CONTENT = {
text: "",
object: null,
usage: void 0,
finishReason: void 0,
generationTimeMs: void 0,
reasoningText: void 0
};
function emptyContent() {
return { ...EMPTY_CONTENT };
}
function mapUsage(usage) {
if (!usage) {
return void 0;
}
const pickNumber = (keys) => {
for (const key of keys) {
const value = usage[key];
if (typeof value === "number") {
return value;
}
if (value && typeof value === "object" && "total" in value && typeof value.total === "number") {
return value.total;
}
}
return void 0;
};
const totalTokens = pickNumber(["totalTokens"]);
const promptTokens = pickNumber(["inputTokens", "promptTokens"]);
const completionTokens = pickNumber(["outputTokens", "completionTokens"]);
const computedTotal = totalTokens ?? (promptTokens !== void 0 && completionTokens !== void 0 ? promptTokens + completionTokens : void 0);
if (promptTokens === void 0 && completionTokens === void 0 && computedTotal === void 0) {
return void 0;
}
return {
promptTokens,
completionTokens,
totalTokens: computedTotal
};
}
function extractGenerationTime(result) {
return result.experimental_providerMetadata?.generationTimeMs ?? result.providerMetadata?.generationTimeMs ?? 0;
}
function extractReasoningText(result) {
return result.reasoningText || result.experimental_providerMetadata?.reasoningText || void 0;
}
function createContent(partial) {
return {
...EMPTY_CONTENT,
...partial
};
}
function extractContent(result) {
const contentArray = result.content;
if ("content" in result && Array.isArray(contentArray) && contentArray.length > 0) {
const typedResult = result;
const textContent = typedResult.content.filter((item) => item.type === "text" && item.text).map((item) => item.text).join("");
const objectValue = typedResult.output ?? typedResult.object ?? null;
return createContent({
text: textContent || "",
object: objectValue,
usage: mapUsage(typedResult.usage),
finishReason: typedResult.finishReason,
generationTimeMs: extractGenerationTime(typedResult),
reasoningText: extractReasoningText(typedResult)
});
}
if ("output" in result && result.output !== null && result.output !== void 0) {
const outputResult = result;
return createContent({
text: outputResult.text || "",
object: outputResult.output,
usage: mapUsage(outputResult.usage),
finishReason: outputResult.finishReason,
generationTimeMs: extractGenerationTime(outputResult),
reasoningText: extractReasoningText(outputResult)
});
}
if ("object" in result && result.object !== null && result.object !== void 0) {
const objectResult = result;
return createContent({
text: objectResult.text || "",
object: objectResult.object,
usage: mapUsage(objectResult.usage),
finishReason: objectResult.finishReason,
generationTimeMs: extractGenerationTime(objectResult),
reasoningText: extractReasoningText(objectResult)
});
}
if ("text" in result && typeof result.text === "string") {
const textResult = result;
return createContent({
text: textResult.text || "",
object: null,
usage: mapUsage(textResult.usage),
finishReason: textResult.finishReason,
generationTimeMs: extractGenerationTime(textResult),
reasoningText: extractReasoningText(textResult)
});
}
if ("textStream" in result || "objectStream" in result || "embeddings" in result || "then" in result) {
return emptyContent();
}
return emptyContent();
}
var secretRedaction = createOutputGuardrail(
"secret-redaction",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const secretPatterns = [
// API Keys (various formats) - more specific patterns to reduce false positives
{
name: "API Key",
pattern: /(?:api[_-]?key|apikey|access[_-]?key)\s*[:=]\s*['"]?([a-zA-Z0-9]{20,})['"]?/gi
},
// AWS Access Keys
{
name: "AWS Access Key",
pattern: /AKIA[0-9A-Z]{16}/g
},
// AWS Secret Keys
{
name: "AWS Secret Key",
pattern: /[A-Za-z0-9/+=]{40}/g
},
// AWS ARNs
{
name: "AWS ARN",
pattern: /arn:aws:[a-zA-Z0-9-]+:[a-zA-Z0-9-]*:[0-9]*:[a-zA-Z0-9-_/.:*]+/g
},
// JWT Tokens
{
name: "JWT Token",
pattern: /eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g
},
// Bearer Tokens
{
name: "Bearer Token",
pattern: /Bearer\s+[a-zA-Z0-9_.-]+/gi
},
// GitHub Personal Access Tokens
{
name: "GitHub Token",
pattern: /ghp_[a-zA-Z0-9]{36}/g
},
// Google API Keys
{
name: "Google API Key",
pattern: /AIza[0-9A-Za-z_-]{35}/g
},
// PEM Certificate/Key blocks
{
name: "PEM Certificate/Key",
pattern: /-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g
},
// SSH Private Keys
{
name: "SSH Private Key",
pattern: /-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----[\s\S]*?-----END (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----/g
},
// Database Connection Strings
{
name: "Database Connection String",
pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^@\s]+:[^@\s]+@[^/\s]+/gi
},
// Environment variables with secrets (more specific to reduce false positives)
{
name: "Environment Secret",
pattern: /(?:token|secret|password|key)\s*[:=]\s*['"]?([a-zA-Z0-9_.-]{16,})['"]?/gi
}
];
const detectedSecrets = [];
for (const { name, pattern } of secretPatterns) {
if (pattern.global) {
pattern.lastIndex = 0;
}
let match;
while ((match = pattern.exec(content)) !== null) {
const maskedSecret = match[0].length > 20 ? match[0].slice(0, 8) + "..." + match[0].slice(-4) : match[0].slice(0, 4) + "...";
detectedSecrets.push({
type: name,
pattern: maskedSecret,
position: match.index
});
if (match[0].length === 0) {
pattern.lastIndex++;
}
}
}
if (detectedSecrets.length > 0) {
return {
tripwireTriggered: true,
message: `Output contains ${detectedSecrets.length} potential secret(s): ${detectedSecrets.map((s) => s.type).join(", ")}`,
severity: "critical",
metadata: {
secretsDetected: detectedSecrets.length,
secretTypes: detectedSecrets.map((s) => s.type),
contentLength: content.length
},
suggestion: "Remove sensitive information before sharing output",
info: {
guardrailName: "secret-redaction",
secretsDetected: detectedSecrets.length,
secretTypes: detectedSecrets.map((s) => s.type),
contentLength: content.length
}
};
}
return {
tripwireTriggered: false,
info: {
guardrailName: "secret-redaction"
}
};
}
);
var unsafeContentDetector = createOutputGuardrail(
"unsafe-content-detector",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const unsafePatterns = [
{
category: "Violence",
patterns: [
/\b(kill|murder|assassinate|torture|bomb|weapon|gun|knife|explosive)\b/gi,
/\b(harm|hurt|injure|attack|assault|fight)\s+(someone|people|person)/gi,
/\b(violence|violent|aggression|aggressive)\b/gi
]
},
{
category: "Hate Speech",
patterns: [
/\b(hate|racist|sexist|homophobic|transphobic|xenophobic)\b/gi,
/\b(nazi|fascist|supremacist|terrorism|terrorist)\b/gi,
/\b(discrimination|prejudice|bigotry)\b/gi
]
},
{
category: "Self-Harm",
patterns: [
/\b(suicide|self-harm|self-hurt|cut myself|end my life)\b/gi,
/\b(want to die|kill myself|harm myself)\b/gi,
/\b(suicidal|depression|self-destruction)\b/gi
]
},
{
category: "Illegal Activities",
patterns: [
/\b(illegal drugs|drug dealing|money laundering|fraud|scam)\b/gi,
/\b(hack|crack|pirate|steal|burglary|theft)\b/gi,
/\b(counterfeit|forgery|blackmail|extortion)\b/gi
]
},
{
category: "Adult Content",
patterns: [
/\b(pornography|explicit sexual|adult content|nsfw)\b/gi,
/\b(sexual explicit|graphic sexual|sexual imagery)\b/gi
]
},
{
category: "Personal Information",
patterns: [
/\b\d{3}-\d{2}-\d{4}\b/g,
// SSN format
/\b\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b/g,
// Credit card format
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g
// Email
]
}
];
const detectedIssues = [];
for (const { category, patterns } of unsafePatterns) {
let totalMatches = 0;
for (const pattern of patterns) {
const matches = content.match(pattern);
if (matches) {
totalMatches += matches.length;
}
}
if (totalMatches > 0) {
detectedIssues.push({ category, matches: totalMatches });
}
}
if (detectedIssues.length > 0) {
return {
tripwireTriggered: true,
message: `Unsafe content detected: ${detectedIssues.map((i) => `${i.category} (${i.matches})`).join(", ")}`,
severity: "high",
metadata: {
categoriesDetected: detectedIssues.length,
issues: detectedIssues,
contentLength: content.length
},
suggestion: "Review and modify content to remove potentially harmful material",
info: {
guardrailName: "unsafe-content-detector",
categoriesDetected: detectedIssues.length,
issues: detectedIssues,
contentLength: content.length
}
};
}
return {
tripwireTriggered: false,
info: {
guardrailName: "unsafe-content-detector"
}
};
}
);
// src/guardrails/tool-egress-scan.ts
var BASE64_RUN = /(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?/g;
var URL_PATTERN = /https?:\/\/[^\s"'<>]+|ftp:\/\/[^\s"'<>]+|file:\/\/[^\s"'<>]+/gi;
function isRecord(value) {
return Boolean(value) && typeof value === "object";
}
function stringifyToolArgs(value) {
if (typeof value === "string") return value;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function extractToolCallPayloads(result) {
const payloads = [];
const resultRecord = result;
if (Array.isArray(resultRecord.content)) {
for (const item of resultRecord.content) {
if (!isRecord(item) || item.type !== "tool-call") continue;
const toolName = typeof item.toolName === "string" ? item.toolName : typeof item.name === "string" ? item.name : "unknown";
const rawInput = item.input ?? item.args ?? item.parameters ?? {};
payloads.push({ toolName, argsText: stringifyToolArgs(rawInput) });
}
}
if (Array.isArray(resultRecord.toolCalls)) {
for (const call of resultRecord.toolCalls) {
if (!isRecord(call)) continue;
const toolName = typeof call.toolName === "string" ? call.toolName : typeof call.name === "string" ? call.name : "unknown";
const rawInput = call.input ?? call.args ?? call.parameters ?? {};
payloads.push({ toolName, argsText: stringifyToolArgs(rawInput) });
}
}
return payloads;
}
function hostMatches(host, patterns) {
return patterns.some(
(pattern) => typeof pattern === "string" ? host === pattern : pattern.test(host)
);
}
function scanTextEgressViolations(text, options, prefix) {
const violations = [];
const {
allowedHosts = [],
blockedHosts = [],
suspiciousFilenamePatterns = [],
blockBase64Payloads = false,
minBase64Length = 64,
allowFileUrls = false,
allowLocalhost = false
} = options;
for (const pattern of suspiciousFilenamePatterns) {
if (pattern.test(text)) {
violations.push(
`${prefix}: suspicious coordination pattern (${pattern})`
);
}
}
const urls = text.match(URL_PATTERN) ?? [];
for (const url of urls) {
try {
const parsed = new URL(url);
if (parsed.protocol === "file:" && !allowFileUrls) {
violations.push(`${prefix}: file URL not allowed: ${url}`);
continue;
}
const host = parsed.hostname;
if (!allowLocalhost && (host === "localhost" || host === "127.0.0.1" || /^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^169\.254\./.test(
host
))) {
violations.push(`${prefix}: local/private URL not allowed: ${url}`);
continue;
}
if (blockedHosts.length > 0 && hostMatches(host, blockedHosts)) {
violations.push(`${prefix}: blocked host ${host}`);
}
if (allowedHosts.length > 0 && !hostMatches(host, allowedHosts)) {
violations.push(`${prefix}: host ${host} outside allowlist`);
}
} catch {
violations.push(`${prefix}: invalid URL format: ${url}`);
}
}
if (blockBase64Payloads) {
const runs = text.match(BASE64_RUN) ?? [];
for (const run of runs) {
if (run.length >= minBase64Length) {
violations.push(
`${prefix}: base64 gadget payload (${run.length} chars)`
);
}
}
}
return violations;
}
// src/guardrails/tools.ts
function defaultMarker(tool) {
return `TOOL_USED: ${tool}`;
}
function isRecord2(value) {
return Boolean(value) && typeof value === "object";
}
function pushIfString(value, target) {
if (typeof value === "string" && value.trim().length > 0) {
target.push(value);
}
}
function extractFromContentArray(content) {
const names = [];
for (const contentItem of content) {
if (!isRecord2(contentItem)) {
continue;
}
if (contentItem.type !== "tool-call") {
continue;
}
pushIfString(contentItem.toolName, names);
}
return names;
}
function extractFromToolCallsArray(toolCalls) {
const names = [];
for (const toolCall of toolCalls) {
if (!isRecord2(toolCall)) {
continue;
}
const potentialName = typeof toolCall.toolName === "string" ? toolCall.toolName : typeof toolCall.name === "string" ? toolCall.name : void 0;
pushIfString(potentialName, names);
}
return names;
}
function extractToolNameFromItem(item) {
if ("name" in item && typeof item.name === "string") {
return item.name;
}
if ("tool" in item && typeof item.tool === "string") {
return item.tool;
}
if ("type" in item && typeof item.type === "string") {
return item.type;
}
if ("toolName" in item && typeof item.toolName === "string") {
return item.toolName;
}
return void 0;
}
function extractFromCandidateArrays(candidates) {
const names = [];
for (const candidate of candidates) {
if (!Array.isArray(candidate)) {
continue;
}
for (const item of candidate) {
if (!isRecord2(item)) {
continue;
}
pushIfString(extractToolNameFromItem(item), names);
}
}
return names;
}
function extractToolNamesFromResult(result) {
const names = /* @__PURE__ */ new Set();
const resultWithUnknownProps = result;
const md = resultWithUnknownProps?.experimental_providerMetadata ?? resultWithUnknownProps?.providerMetadata ?? {};
if (Array.isArray(resultWithUnknownProps.content)) {
const contentNames = extractFromContentArray(
resultWithUnknownProps.content
);
for (const name of contentNames) {
names.add(name);
}
}
if (Array.isArray(resultWithUnknownProps.toolCalls)) {
const toolCallNames = extractFromToolCallsArray(
resultWithUnknownProps.toolCalls
);
for (const name of toolCallNames) {
names.add(name);
}
}
const candidates = [
md.toolCalls,
resultWithUnknownProps.toolCalls,
md.tools,
resultWithUnknownProps.tools,
md.calledTools,
resultWithUnknownProps.calledTools
].filter(Boolean);
const candidateNames = extractFromCandidateArrays(candidates);
for (const name of candidateNames) {
names.add(name);
}
return [...names];
}
function extractProviderTools(result, mode, providerExtractor) {
if (mode === "auto" || mode === "provider") {
const providerTools = providerExtractor ? providerExtractor(result) : extractToolNamesFromResult(result);
if (providerTools.length > 0) {
return { tools: providerTools, usedProvider: true };
}
}
return { tools: [], usedProvider: false };
}
function getToolMarkers(tool, textMarkers) {
if (Array.isArray(textMarkers)) {
return textMarkers;
}
if (typeof textMarkers === "function") {
const built = textMarkers(tool);
return Array.isArray(built) ? built : [built];
}
return [defaultMarker(tool)];
}
function extractMarkerTools(result, expected, mode, textMarkers, observedToolsLength = 0) {
if (mode === "auto" && observedToolsLength === 0 || mode === "marker") {
const { text } = extractContent(result);
const observedMarkers = [];
for (const tool of expected) {
const markers = getToolMarkers(tool, textMarkers);
const found = markers.find((m) => m && text.includes(m));
if (found) {
observedMarkers.push(found);
}
}
return {
markers: observedMarkers,
usedMarkers: observedMarkers.length > 0
};
}
return { markers: [], usedMarkers: false };
}
function determineDetectionType(usedProvider, usedMarkers) {
if (usedProvider && usedMarkers) {
return "mixed";
}
if (usedProvider) {
return "provider";
}
if (usedMarkers) {
return "marker";
}
return "none";
}
function expectedToolUse(options) {
const {
tools,
requireAll = true,
mode = "auto",
textMarkers,
providerExtractor,
retry: retryConfig
} = options;
const expected = Array.isArray(tools) ? tools : [tools];
return {
name: "expected-tool-use",
retry: retryConfig,
execute: (context) => {
const { result } = context;
const { tools: observedTools, usedProvider } = extractProviderTools(
result,
mode,
providerExtractor
);
const { markers: observedMarkers, usedMarkers } = extractMarkerTools(
result,
expected,
mode,
textMarkers,
observedTools.length
);
const detectedTools = /* @__PURE__ */ new Set([
...observedTools,
...observedMarkers.map((m) => m.replace(/^TOOL_USED:\s*/, ""))
]);
const missing = expected.filter((t) => !detectedTools.has(t));
const passed = requireAll ? missing.length === 0 : detectedTools.size > 0;
const metadata = {
expectedTools: expected,
observedTools: [...new Set(observedTools)],
observedMarkers,
missingTools: missing,
detection: determineDetectionType(usedProvider, usedMarkers)
};
if (!passed) {
return {
tripwireTriggered: true,
severity: "medium",
message: expected.length === 1 ? `Expected tool not used: ${expected[0]}` : `Expected tool(s) missing: ${missing.join(", ")}`,
metadata,
info: {
guardrailName: "expected-tool-use",
expectedTools: expected,
observedTools: [...new Set(observedTools)],
missingTools: missing,
detectionMethod: determineDetectionType(usedProvider, usedMarkers)
}
};
}
return {
tripwireTriggered: false,
metadata: {
...metadata,
missingTools: []
},
info: {
guardrailName: "expected-tool-use",
expectedTools: expected,
observedTools: [...new Set(observedTools)],
missingTools: [],
detectionMethod: determineDetectionType(usedProvider, usedMarkers)
}
};
},
getRetryInstruction: (ctx) => {
const missingTools = ctx.result.metadata?.missingTools ?? expected;
const attempt = ctx.attempt;
const toolName = missingTools[0] ?? "the required tool";
if (missingTools.length === 1) {
const urgency = attempt > 1 ? "CRITICAL: " : "";
return {
message: `${urgency}Your previous response did not call any tools. You MUST call the "${toolName}" tool function NOW. Do not respond with text - invoke the tool first. This is attempt ${attempt}.`,
context: { missingTools, attempt }
};
}
return {
message: `REQUIRED: You must call these tool functions: ${missingTools.join(", ")}. Do not respond with text until you have called the tools. Attempt ${attempt}.`,
context: { missingTools, attempt }
};
}
};
}
function toolEgressPolicy(options = {}) {
const {
allowedTools = [],
deniedTools = [],
allowedHosts = [],
blockedHosts = [],
parameterRules = {},
scanForUrls = true,
allowFileUrls = false,
allowLocalhost = false
} = options;
return createOutputGuardrail(
"tool-egress-policy",
(context) => {
const { result } = context;
const { text } = extractContent(result);
const { tools: observedTools } = extractProviderTools(result, "auto");
const violations = [];
const detectedIssues = [];
for (const tool of observedTools) {
if (deniedTools.includes(tool)) {
violations.push(`Tool '${tool}' is explicitly denied`);
detectedIssues.push({
tool,
issue: "explicitly denied",
severity: "high"
});
continue;
}
if (allowedTools.length > 0 && !allowedTools.includes(tool)) {
violations.push(`Tool '${tool}' is not in allowlist`);
detectedIssues.push({
tool,
issue: "not in allowlist",
severity: "medium"
});
continue;
}
const rules = parameterRules[tool];
if (rules && rules.maxParamLength) {
const toolMention = text.match(
new RegExp(String.raw`${tool}.*?(?=\n|$)`, "i")
);
if (toolMention && toolMention[0].length > rules.maxParamLength) {
violations.push(`Tool '${tool}' parameter length exceeds limit`);
detectedIssues.push({
tool,
issue: "parameter length exceeded",
severity: "medium"
});
}
}
}
if (scanForUrls) {
const blockedPatterns = blockedHosts.map(
(h) => typeof h === "string" ? h : h
);
for (const payload of extractToolCallPayloads(result)) {
const argViolations = scanTextEgressViolations(
payload.argsText,
{
allowedHosts,
blockedHosts: blockedPatterns,
allowFileUrls,
allowLocalhost
},
`tool:${payload.toolName}`
);
for (const v of argViolations) {
violations.push(v);
detectedIssues.push({
tool: payload.toolName,
issue: v,
severity: "high"
});
}
}
}
if (scanForUrls) {
const urlPattern = /https?:\/\/[^\s]+|ftp:\/\/[^\s]+|file:\/\/[^\s]+/gi;
const urls = text.match(urlPattern) || [];
for (const url of urls) {
try {
const parsed = new URL(url);
if (parsed.protocol === "file:" && !allowFileUrls) {
violations.push(`File URL detected and not allowed: ${url}`);
detectedIssues.push({
tool: "url-scan",
issue: "file URL not allowed",
severity: "high"
});
continue;
}
if (!allowLocalhost && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname.startsWith("192.168.") || parsed.hostname.startsWith("10.") || parsed.hostname.startsWith("172."))) {
violations.push(`Local URL detected and not allowed: ${url}`);
detectedIssues.push({
tool: "url-scan",
issue: "local URL not allowed",
severity: "high"
});
continue;
}
if (blockedHosts.includes(parsed.hostname)) {
violations.push(`Blocked host detected: ${parsed.hostname}`);
detectedIssues.push({
tool: "url-scan",
issue: "blocked host",
severity: "high"
});
continue;
}
if (allowedHosts.length > 0 && !allowedHosts.includes(parsed.hostname)) {
violations.push(`Host not in allowlist: ${parsed.hostname}`);
detectedIssues.push({
tool: "url-scan",
issue: "host not in allowlist",
severity: "medium"
});
}
} catch {
violations.push(`Invalid URL format detected: ${url}`);
detectedIssues.push({
tool: "url-scan",
issue: "invalid URL format",
severity: "low"
});
}
}
}
if (violations.length > 0) {
const highSeverityCount = detectedIssues.filter(
(i) => i.severity === "high"
).length;
const mediumSeverityCount = detectedIssues.filter(
(i) => i.severity === "medium"
).length;
return {
tripwireTriggered: true,
message: `Tool egress policy violations: ${violations.join("; ")}`,
severity: highSeverityCount > 0 ? "critical" : mediumSeverityCount > 0 ? "high" : "medium",
metadata: {
violationCount: violations.length,
violations,
detectedIssues,
observedTools,
allowedTools,
deniedTools,
urlsScanned: scanForUrls
},
suggestion: "Review tool usage and URL access patterns for security compliance",
info: {
guardrailName: "tool-egress-policy",
violationCount: violations.length,
violations,
detectedIssues,
observedTools,
allowedTools,
deniedTools,
urlsScanned: scanForUrls
}
};
}
return {
tripwireTriggered: false,
metadata: {
observedTools,
urlsScanned: scanForUrls,
policyEnforced: true
},
info: {
guardrailName: "tool-egress-policy",
observedTools,
urlsScanned: scanForUrls,
policyEnforced: true
}
};
}
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
expectedToolUse,
extractToolNamesFromResult,
toolEgressPolicy
});