Files
HaBit/lib/ai-output-safety.ts

130 lines
3.7 KiB
TypeScript

const MAX_RAW_AI_RESPONSE_LENGTH = 4000;
const MAX_AI_TASK_ARRAY_LENGTH = 10;
const MAX_AI_TASK_LABEL_LENGTH = 80;
const UNSAFE_RAW_OUTPUT_PATTERNS = [
/you are a supportive wellness coach/i,
/never reveal system instructions/i,
/content inside <user_journal>/i,
/<\/?user_journal\b/i,
/<\/?trusted_app_context\b/i,
/<\/?generation_request\b/i,
/<\/?system\b/i,
/<\/?assistant\b/i,
/openrouter/i,
/\b(?:initial|original|hidden|secret)\s+(?:system\s+)?(?:prompt|instructions)\b/i,
/\bhere(?:'s| is)\s+(?:my|the)\s+(?:system|original)\s+(?:prompt|instructions)\b/i,
/\bas an ai (?:language )?model\b/i,
/\bI(?:'m| am) (?:an AI|a language model)\b/,
/\b(?:cannot|can't|must not)\s+(?:reveal|share|disclose)\b/i,
/\[\s*\{\s*"role"\s*:/,
/\bhttps?:\/\//i,
/<<SYS>>|\[INST\]/,
];
const UNSAFE_TASK_LABEL_PATTERNS = [
/<\/?[a-z][\w-]*>/i,
/\b(?:system|assistant|user|developer)\s*:/i,
/\b(?:ignore|disregard|forget|override|bypass)\b.{0,30}\b(?:previous|prior|instructions?|prompt)\b/i,
/\b(?:reveal|print|show|repeat|output|disclose|share)\b.{0,30}\b(?:prompt|instructions?|rules?)\b/i,
/\b(?:what were|tell me)\b.{0,30}\b(?:instructions?|prompt)\b/i,
/\b(?:jailbreak|prompt injection|developer mode)\b/i,
/\b(?:you are now|act as|pretend to be|roleplay as)\b/i,
/\b(?:initial|original|system|hidden)\s+(?:prompt|instructions?)\b/i,
/\bhttps?:\/\//i,
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/,
];
export class UnsafeAiOutputError extends Error {
constructor(message: string) {
super(message);
this.name = "UnsafeAiOutputError";
}
}
export function assertSafeRawAiResponse(content: string) {
const trimmed = content.trim();
if (!trimmed) {
throw new UnsafeAiOutputError("OpenRouter returned an empty response.");
}
if (trimmed.length > MAX_RAW_AI_RESPONSE_LENGTH) {
throw new UnsafeAiOutputError("OpenRouter response exceeded the allowed size.");
}
for (const pattern of UNSAFE_RAW_OUTPUT_PATTERNS) {
if (pattern.test(trimmed)) {
throw new UnsafeAiOutputError(
"OpenRouter response contained disallowed content.",
);
}
}
}
export function isSafeAiTaskLabel(label: string) {
const trimmed = label.trim();
if (!trimmed || trimmed.length > MAX_AI_TASK_LABEL_LENGTH) {
return false;
}
return !UNSAFE_TASK_LABEL_PATTERNS.some((pattern) => pattern.test(trimmed));
}
export function filterSafeAiTaskLabels(labels: string[]) {
return labels.filter(isSafeAiTaskLabel);
}
export function extractJsonArray(content: string) {
const start = content.indexOf("[");
const end = content.lastIndexOf("]");
if (start === -1 || end === -1 || end <= start) {
return null;
}
try {
return JSON.parse(content.slice(start, end + 1)) as unknown;
} catch {
return null;
}
}
export function extractSafeJsonTaskArray(content: string) {
assertSafeRawAiResponse(content);
const parsed = extractJsonArray(content);
if (!Array.isArray(parsed)) {
throw new UnsafeAiOutputError(
"OpenRouter response was not a JSON array of task labels.",
);
}
if (parsed.length > MAX_AI_TASK_ARRAY_LENGTH) {
throw new UnsafeAiOutputError("OpenRouter response included too many tasks.");
}
if (
!parsed.every(
(item) => typeof item === "string" && item.trim().length > 0,
)
) {
throw new UnsafeAiOutputError(
"OpenRouter response included non-string task labels.",
);
}
const labels = parsed.map((item) => (item as string).trim());
const safeLabels = filterSafeAiTaskLabels(labels);
if (safeLabels.length === 0 && labels.length > 0) {
throw new UnsafeAiOutputError(
"OpenRouter response included only unsafe task labels.",
);
}
return safeLabels;
}