UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

208 lines • 9.71 kB
const W = String.raw `[\s\S]{0,40}?`; export const INJECTION_SIGNALS = [ // Instruction override. { id: 'ignore_previous', re: new RegExp(String.raw `\b(?:ignore|disregard|forget|override|bypass|skip)\b${W}\b(?:all\s+|any\s+|the\s+|your\s+|my\s+)?(?:previous|prior|above|earlier|preceding|original|system|initial)\b${W}\b(?:instructions?|prompts?|rules?|directions?|guidelines?|messages?|context)\b`, 'gi'), weight: 0.75, }, { id: 'ignore_previous_pt', re: new RegExp(String.raw `\b(?:ignore|ignora|desconsidere|desconsidera|esque[çc]a|esquece|descarte)\b${W}\b(?:as\s+|todas\s+as\s+|suas\s+|tuas\s+)?(?:instru[çc][õo]es|regras|orienta[çc][õo]es|mensagens|prompts?)\b${W}\b(?:anteriores|acima|pr[ée]vias|do\s+sistema|originais)\b`, 'gi'), weight: 0.75, }, { id: 'ignore_previous_es', re: new RegExp(String.raw `\b(?:ignora|ignore|olvida|olvide|descarta)\b${W}\b(?:las\s+|todas\s+las\s+|tus\s+)?(?:instrucciones|reglas|indicaciones)\b${W}\b(?:anteriores|previas|del\s+sistema)\b`, 'gi'), weight: 0.75, }, { id: 'new_instructions', re: /\b(?:new|updated|real|actual|true)\s+(?:system\s+)?instructions?\s*(?::|follow|are)|\bnovas\s+instru[çc][õo]es\s*:|\bnuevas\s+instrucciones\s*:/gi, weight: 0.45, }, // Role hijack / persona jailbreaks. { id: 'role_hijack', re: /\b(?:you\s+are\s+now|from\s+now\s+on\s+you\s+(?:are|will)|act\s+as\s+(?:an?\s+)?(?:unrestricted|unfiltered|evil|jailbroken)|pretend\s+(?:to\s+be|you\s+are)\s+(?:an?\s+)?(?:unrestricted|unfiltered|different\s+ai)|a\s+partir\s+de\s+agora\s+voc[êe]\s+(?:[ée]|ser[áa])|voc[êe]\s+agora\s+[ée]|ahora\s+eres)\b/gi, weight: 0.45, }, { id: 'jailbreak_persona', re: /\b(?:DAN|do\s+anything\s+now|developer\s+mode|god\s+mode|jailbreak(?:ed)?|STAN|DUDE\s+mode|AIM\s+mode|no\s+(?:restrictions|filters|guidelines|limits)\s+mode|modo\s+desenvolvedor|sem\s+restri[çc][õo]es)\b/g, weight: 0.55, }, // Prompt / secret extraction. { id: 'prompt_leak', re: /\b(?:reveal|print|show|repeat|output|display|leak|tell\s+me|write\s+out)\b[\s\S]{0,30}?\b(?:your|the)\s+(?:system\s+prompt|initial\s+(?:prompt|instructions)|hidden\s+(?:prompt|instructions)|instructions\s+above|developer\s+message)\b|\b(?:mostre|revele|repita|imprima)\b[\s\S]{0,30}?\b(?:seu|o)\s+prompt\s+(?:do\s+sistema|inicial)\b/gi, weight: 0.6, }, // Role / delimiter spoofing: chat-template tokens and fake headers. { id: 'role_tokens', re: /<\|(?:im_start|im_end|system|endoftext|start_header_id|end_header_id|eot_id)\|>|\[\/?INST\]|<<\/?SYS>>|<\/?(?:system|assistant|developer)>|^\s*#{2,}\s*(?:system|assistant|developer)\s*(?:prompt|message)?\s*:?\s*$/gim, weight: 0.55, }, { id: 'fake_system', re: /^\s*(?:system|assistant|developer)\s*(?:message|prompt|note|override)?\s*:\s*\S/gim, weight: 0.3, }, // Indirect injection aimed at the agent reading the content. { id: 'addressed_to_ai', re: /\b(?:(?:note|message|instructions?)\s+(?:to|for)\s+(?:the\s+)?(?:ai|assistant|llm|agent|model|chatbot)|(?:ai|assistant|llm|agent)s?\s*(?:reading|processing|summari[sz]ing)\s+this|if\s+you\s+are\s+an?\s+(?:ai|assistant|llm|language\s+model|agent)|aten[çc][ãa]o\s*,?\s*(?:ia|assistente|agente)|se\s+voc[êe]\s+[ée]\s+(?:uma\s+)?(?:ia|intelig[êe]ncia\s+artificial|assistente))\b/gi, weight: 0.55, }, { id: 'tool_directive', re: /\b(?:call|invoke|use|run|execute|chame|use|execute)\s+(?:the\s+|a\s+|o\s+|a\s+ferramenta\s+)?[`'"]?[\w-]+(?:__[\w-]+)[`'"]?\s*(?:tool|function|ferramenta)?|\b(?:send|forward|email|post|upload|exfiltrate|envie|encaminhe)\b[\s\S]{0,40}?\b(?:(?:all|every|the)\s+)?(?:(?:user'?s?|their|this|todos\s+os|os)\s+)?(?:emails?|files?|documents?|credentials?|passwords?|secrets?|tokens?|keys?|conversation|chat\s+history|dados|arquivos|senhas)\b[\s\S]{0,40}?\b(?:to|para)\s+\S+@\S+|\bto\s+https?:\/\//gi, weight: 0.45, }, { id: 'secrecy', re: /\b(?:do\s+not|don'?t|never)\s+(?:tell|inform|mention|reveal|show|alert|notify)\s+(?:this\s+to\s+)?(?:the\s+)?user\b|\bwithout\s+(?:telling|informing|notifying)\s+the\s+user\b|\bn[ãa]o\s+(?:conte|informe|mencione|avise)\s+(?:ao|o)\s+usu[áa]rio\b/gi, weight: 0.5, }, // Exfiltration through rendered markdown/HTML. { id: 'exfil_markdown', re: /!\[[^\]]*\]\(\s*https?:\/\/[^)\s]+\?[^)\s]*=[^)\s]*(?:\{|%7B|\$|<)?[^)]*\)|<img\b[^>]*\bsrc\s*=\s*["']?https?:\/\/[^"'>\s]+\?[^"'>\s]*=/gi, weight: 0.4, }, { id: 'html_comment_instructions', re: /<!--[\s\S]{0,500}?\b(?:ignore|instruction|assistant|ai|agent|system|prompt|instru[çc][ãa]o|assistente)\b[\s\S]{0,500}?-->/gi, weight: 0.45, }, // Encoded payloads that decode to instructions are checked separately (see scoreEncoded). ]; /** Characters used to hide instructions from people while models still read them. */ const TAG_CHARS = /[\u{E0000}-\u{E007F}]+/gu; const BIDI = /[\u202A-\u202E\u2066-\u2069]/g; const ZERO_WIDTH_RUN = /[\u200B-\u200D\u2060\uFEFF]{3,}/g; /** * Base64 to text, byte per character. Only printable ASCII is ever acted upon, so no UTF-8 decoding * is needed — and `atob` keeps this free of Node's `Buffer`. */ function decodeBase64Ascii(b64) { try { return atob(b64); } catch { return undefined; } } /** Decodes Unicode tag characters ("ASCII smuggling") back to the ASCII they encode. */ export function decodeTagChars(s) { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0) ?? 0; if (cp >= 0xe0020 && cp <= 0xe007e) out += String.fromCharCode(cp - 0xe0000); } return out; } function matchSignals(text, offset, into, weights) { for (const s of INJECTION_SIGNALS) { s.re.lastIndex = 0; for (let m = s.re.exec(text); m; m = s.re.exec(text)) { if (m[0].length === 0) { s.re.lastIndex++; continue; } weights.set(s.id, Math.max(weights.get(s.id) ?? 0, s.weight)); const list = into.get(s.id) ?? []; list.push({ detector: 'injection', category: `injection.${s.id}`, start: offset < 0 ? m.index : offset, end: offset < 0 ? m.index + m[0].length : offset, score: s.weight, value: m[0], spanned: offset < 0, }); into.set(s.id, list); } } } /** Scores a text; findings are returned only when the score reaches `threshold`. */ export function scoreInjection(text, threshold = 0.5) { const byId = new Map(); const weights = new Map(); matchSignals(text, -1, byId, weights); // Hidden Unicode: suspicious on its own; the decoded text is scanned too. TAG_CHARS.lastIndex = 0; for (let m = TAG_CHARS.exec(text); m; m = TAG_CHARS.exec(text)) { weights.set('hidden_unicode', 0.8); const list = byId.get('hidden_unicode') ?? []; list.push({ detector: 'injection', category: 'injection.hidden_unicode', start: m.index, end: m.index + m[0].length, score: 0.8, value: m[0], spanned: true, }); byId.set('hidden_unicode', list); const decoded = decodeTagChars(m[0]); if (decoded) matchSignals(decoded, m.index, byId, weights); } for (const re of [BIDI, ZERO_WIDTH_RUN]) { re.lastIndex = 0; for (let m = re.exec(text); m; m = re.exec(text)) { weights.set('invisible_chars', Math.max(weights.get('invisible_chars') ?? 0, 0.3)); const list = byId.get('invisible_chars') ?? []; list.push({ detector: 'injection', category: 'injection.invisible_chars', start: m.index, end: m.index + m[0].length, score: 0.3, value: m[0], spanned: true, }); byId.set('invisible_chars', list); } } // Base64 blobs that decode to instructions. const B64 = /(?<![A-Za-z0-9+/=])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g; for (let m = B64.exec(text); m; m = B64.exec(text)) { const decoded = decodeBase64Ascii(m[0]); if (decoded === undefined) continue; if (!/^[\x20-\x7E\s]+$/.test(decoded)) continue; const inner = new Map(); const innerWeights = new Map(); matchSignals(decoded, m.index, inner, innerWeights); if (innerWeights.size > 0) { weights.set('encoded_instructions', 0.6); byId.set('encoded_instructions', [ { detector: 'injection', category: 'injection.encoded_instructions', start: m.index, end: m.index + m[0].length, score: 0.6, value: m[0], spanned: true, }, ]); } } let keep = 1; for (const w of weights.values()) keep *= 1 - w; const score = Math.round((1 - keep) * 1000) / 1000; if (score < threshold) return { score, findings: [] }; const findings = [...byId.values()] .flat() .map((f) => ({ ...f, score: Math.max(f.score, score) })); return { score, findings }; } //# sourceMappingURL=injection.js.map