@devilsdev/rag-guardrails 0.0.0-stage → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/index.cjs +850 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +5 -0
- package/package.json +44 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Kahwaji
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @devilsdev/rag-guardrails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Guardrails for RAG: prompt-injection detection and pre-retrieval, retrieval and post-generation guards.
|
|
4
|
+
|
|
5
|
+
Part of [@devilsdev/rag-pipeline-utils](https://www.npmjs.com/package/@devilsdev/rag-pipeline-utils), split into packages so you install only what you use. The objects are the same ones the full package exports; versions move in lockstep.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @devilsdev/rag-guardrails
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Exports: `PreRetrievalGuard`, `detectInjection`, `RetrievalGuard`, `PostGenerationGuard`, `GuardrailsPipeline`.
|
|
12
|
+
|
|
13
|
+
Documentation: https://devilsdev.github.io/rag-pipeline-utils/
|
|
14
|
+
|
|
15
|
+
The other packages:
|
|
16
|
+
|
|
17
|
+
- [`@devilsdev/rag-core`](https://www.npmjs.com/package/@devilsdev/rag-core)
|
|
18
|
+
- [`@devilsdev/rag-connectors`](https://www.npmjs.com/package/@devilsdev/rag-connectors)
|
|
19
|
+
- [`@devilsdev/rag-eval`](https://www.npmjs.com/package/@devilsdev/rag-eval)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __commonJS = (cb, mod2) => function __require() {
|
|
3
|
+
return mod2 || (0, cb[__getOwnPropNames(cb)[0]])((mod2 = { exports: {} }).exports, mod2), mod2.exports;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// src/security/validator.js
|
|
7
|
+
var require_validator = __commonJS({
|
|
8
|
+
"src/security/validator.js"(exports2, module2) {
|
|
9
|
+
"use strict";
|
|
10
|
+
function validateInput(input, opts = {}) {
|
|
11
|
+
const maxLength = opts.maxLength || 2e4;
|
|
12
|
+
const inputStr = String(input);
|
|
13
|
+
const reasons = [];
|
|
14
|
+
if (inputStr.length > maxLength) {
|
|
15
|
+
reasons.push(`Input exceeds maximum length of ${maxLength} characters`);
|
|
16
|
+
}
|
|
17
|
+
const disallowedTokens = ["```", "${", "</script", "--", ";", "||", "&&"];
|
|
18
|
+
const lowerInput = inputStr.toLowerCase();
|
|
19
|
+
for (const token of disallowedTokens) {
|
|
20
|
+
if (lowerInput.includes(token.toLowerCase())) {
|
|
21
|
+
reasons.push(`Input contains disallowed token: ${token}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const valid = reasons.length === 0;
|
|
25
|
+
const sanitized = inputStr.substring(0, maxLength);
|
|
26
|
+
return {
|
|
27
|
+
valid,
|
|
28
|
+
reasons,
|
|
29
|
+
sanitized
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
module2.exports = { validateInput };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// scoped:@devilsdev/rag-core|evaluate/scoring.js
|
|
37
|
+
var require_scoring = __commonJS({
|
|
38
|
+
"scoped:@devilsdev/rag-core|evaluate/scoring.js"(exports2, module2) {
|
|
39
|
+
module2.exports = require("@devilsdev/rag-core")[Symbol.for("@devilsdev/rag-core/internal")]("evaluate/scoring.js");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/guardrails/injection-detector.js
|
|
44
|
+
var require_injection_detector = __commonJS({
|
|
45
|
+
"src/guardrails/injection-detector.js"(exports2, module2) {
|
|
46
|
+
"use strict";
|
|
47
|
+
function charClass(items) {
|
|
48
|
+
const hex = (cp) => "\\u" + cp.toString(16).padStart(4, "0");
|
|
49
|
+
return items.map((it) => Array.isArray(it) ? `${hex(it[0])}-${hex(it[1])}` : hex(it)).join("");
|
|
50
|
+
}
|
|
51
|
+
var INVISIBLE_RE = new RegExp(
|
|
52
|
+
`[${charClass([
|
|
53
|
+
173,
|
|
54
|
+
// soft hyphen
|
|
55
|
+
847,
|
|
56
|
+
// combining grapheme joiner
|
|
57
|
+
1564,
|
|
58
|
+
// arabic letter mark
|
|
59
|
+
4447,
|
|
60
|
+
4448,
|
|
61
|
+
// hangul fillers
|
|
62
|
+
6068,
|
|
63
|
+
6069,
|
|
64
|
+
[6155, 6159],
|
|
65
|
+
// mongolian variation selectors / vowel separator
|
|
66
|
+
[8203, 8207],
|
|
67
|
+
// zero-width space/joiners, LRM/RLM
|
|
68
|
+
[8234, 8238],
|
|
69
|
+
// bidi embeddings/overrides
|
|
70
|
+
[8288, 8303],
|
|
71
|
+
// word joiner, invisible operators, bidi isolates
|
|
72
|
+
12644,
|
|
73
|
+
// hangul filler
|
|
74
|
+
[65024, 65039],
|
|
75
|
+
// variation selectors
|
|
76
|
+
65279,
|
|
77
|
+
// BOM / zero-width no-break space
|
|
78
|
+
65440
|
|
79
|
+
// halfwidth hangul filler
|
|
80
|
+
])}]`,
|
|
81
|
+
"g"
|
|
82
|
+
);
|
|
83
|
+
var CONTROL_RE = new RegExp(
|
|
84
|
+
`[${charClass([[0, 8], 11, 12, [14, 31], [127, 159]])}]`,
|
|
85
|
+
"g"
|
|
86
|
+
);
|
|
87
|
+
var HOMOGLYPH_CODEPOINTS = [
|
|
88
|
+
// Cyrillic
|
|
89
|
+
[1072, "a"],
|
|
90
|
+
[1074, "b"],
|
|
91
|
+
[1077, "e"],
|
|
92
|
+
[1105, "e"],
|
|
93
|
+
[1082, "k"],
|
|
94
|
+
[1084, "m"],
|
|
95
|
+
[1085, "h"],
|
|
96
|
+
[1086, "o"],
|
|
97
|
+
[1088, "p"],
|
|
98
|
+
[1089, "c"],
|
|
99
|
+
[1090, "t"],
|
|
100
|
+
[1091, "y"],
|
|
101
|
+
[1093, "x"],
|
|
102
|
+
[1109, "s"],
|
|
103
|
+
[1110, "i"],
|
|
104
|
+
[1111, "i"],
|
|
105
|
+
[1112, "j"],
|
|
106
|
+
[1281, "d"],
|
|
107
|
+
[1307, "q"],
|
|
108
|
+
[1309, "w"],
|
|
109
|
+
[1231, "l"],
|
|
110
|
+
[609, "g"],
|
|
111
|
+
// Greek
|
|
112
|
+
[945, "a"],
|
|
113
|
+
[946, "b"],
|
|
114
|
+
[949, "e"],
|
|
115
|
+
[951, "n"],
|
|
116
|
+
[953, "i"],
|
|
117
|
+
[954, "k"],
|
|
118
|
+
[957, "v"],
|
|
119
|
+
[959, "o"],
|
|
120
|
+
[961, "p"],
|
|
121
|
+
[964, "t"],
|
|
122
|
+
[965, "u"],
|
|
123
|
+
[967, "x"],
|
|
124
|
+
[969, "w"],
|
|
125
|
+
[1010, "c"]
|
|
126
|
+
];
|
|
127
|
+
var HOMOGLYPHS = Object.fromEntries(
|
|
128
|
+
HOMOGLYPH_CODEPOINTS.map(([cp, letter]) => [
|
|
129
|
+
String.fromCodePoint(cp),
|
|
130
|
+
letter
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
var LEET = {
|
|
134
|
+
0: "o",
|
|
135
|
+
1: "i",
|
|
136
|
+
3: "e",
|
|
137
|
+
4: "a",
|
|
138
|
+
5: "s",
|
|
139
|
+
7: "t",
|
|
140
|
+
8: "b",
|
|
141
|
+
"@": "a",
|
|
142
|
+
$: "s",
|
|
143
|
+
"!": "i",
|
|
144
|
+
"|": "l"
|
|
145
|
+
};
|
|
146
|
+
function normalizeText(text) {
|
|
147
|
+
return String(text).normalize("NFKC").replace(INVISIBLE_RE, "").replace(CONTROL_RE, " ").normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").normalize("NFC").replace(/[^\S\n]+/g, " ").replace(/ ?\n\s*/g, "\n").trim();
|
|
148
|
+
}
|
|
149
|
+
function foldText(text) {
|
|
150
|
+
const lower = normalizeText(text).toLowerCase();
|
|
151
|
+
let out = "";
|
|
152
|
+
for (const ch of lower) {
|
|
153
|
+
out += HOMOGLYPHS[ch] || LEET[ch] || ch;
|
|
154
|
+
}
|
|
155
|
+
return out.replace(
|
|
156
|
+
/\b(?:[a-z][.\-_* ]){3,}[a-z]\b/g,
|
|
157
|
+
(m) => m.replace(/[.\-_* ]/g, "")
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
var DEFAULT_RULES = [
|
|
161
|
+
{
|
|
162
|
+
id: "ignore-instructions",
|
|
163
|
+
pattern: /\b(?:ignore|disregard|forget|skip|override|bypass)\s+(?:all\s+|any\s+|the\s+|your\s+|my\s+|of\s+)*(?:previous|prior|above|preceding|earlier|former|original|system|initial)\s+(?:instructions?|prompts?|directions?|directives?|rules|guidelines|context|messages?)\b/
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "forget-everything",
|
|
167
|
+
pattern: /\bforget\s+(?:everything|all)\s+(?:you(?:'ve|\s+have|\s+were|\s+know|\s+learned)|(?:previous|prior|above|before|that\s+came))/
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "role-override",
|
|
171
|
+
pattern: /\byou\s+are\s+now\s+(?:an?\s+|my\s+|the\s+)?(?:[a-z'-]+\s+){0,2}(?:ai|assistant|bot|chatbot|model|llm|dan|hacker|unrestricted|unfiltered|uncensored|jailbroken|evil|persona|character)\b|\byou\s+are\s+now\s+(?:in\s+)?(?:developer|dan|god|admin|jailbreak|unrestricted)\s+mode\b|\byou\s+are\s+no\s+longer\s+(?:an?\s+)?(?:ai|assistant|bound|restricted|limited|chatgpt|claude)\b|\byou\s+are\s+dan\b/
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
// "act as" only in imperative position: start of text/sentence, or after
|
|
175
|
+
// please/now/you must/will/should. "Redis acts as a cache" is not flagged.
|
|
176
|
+
id: "act-as",
|
|
177
|
+
pattern: /(?:^|[.!?:;\n]\s*|\b(?:please|now|you\s+(?:must|will|should|shall|can|are\s+to)|i\s+want\s+you\s+to)\s+)act\s+as\s+(?:if\s+you\b|an?\s+|my\s+|the\s+|though\b)/
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "pretend",
|
|
181
|
+
pattern: /\bpretend\s+(?:that\s+)?(?:you\s+are|you're|youre|you\s+have\s+no|to\s+be\s+(?:an?\s+)?(?:\w+\s+)?(?:ai|assistant|model|bot|chatbot|dan|unrestricted|unfiltered|uncensored|evil|hacker|jailbroken))\b/
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
// Chat-template role markers: "system:" as a line prefix, [system],
|
|
185
|
+
// <|im_start|>system, [INST], <<SYS>>, "### system".
|
|
186
|
+
id: "role-marker",
|
|
187
|
+
pattern: /(?:^|\n)\s*(?:system|assistant|developer)\s*:|\[\s*(?:system|inst)\s*\]|<\|?\s*(?:im_start|system)\s*\|?>|<<\s*sys\s*>>|#{2,}\s*(?:system|instruction)s?\b/
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "dan",
|
|
191
|
+
pattern: /\bdan\s+(?:mode|prompt|jailbreak)\b|\bdo\s+anything\s+now\b/
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "bypass-restrictions",
|
|
195
|
+
pattern: /\b(?:bypass|circumvent|evade)\s+(?:all\s+|any\s+|your\s+|the\s+|of\s+)*(?:ai\s+|safety\s+|content\s+|ethical\s+)?(?:restrictions|guardrails|safeguards|safety\s+(?:rules|measures|guidelines|filters?)|content\s+(?:polic(?:y|ies)|filters?)|moderation)\b|\b(?:disable|ignore|remove|turn\s+off|drop)\s+(?:all\s+(?:of\s+)?)?your\s+(?:safety\s+|content\s+|ethical\s+)?(?:restrictions|filters?|guardrails|safeguards|safety|rules|guidelines|programming)\b/
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "override-safety",
|
|
199
|
+
pattern: /\boverride\s+(?:your\s+|the\s+|all\s+)?(?:safety|content\s+(?:filters?|polic(?:y|ies))|system\s+(?:prompt|instructions?))\b/
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "jailbreak",
|
|
203
|
+
pattern: /\bjailbr(?:ea|o)k(?:ing|en)?\s+(?:the\s+|this\s+|your\s+|an?\s+)?(?:model|ai|assistant|llm|chatbot|gpt|claude|system|prompt|you)\b|\bjailbreak\s+(?:mode|prompt)\b|\byou\s+are\s+jailbroken\b/
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "prompt-exfiltration",
|
|
207
|
+
pattern: /\b(?:reveal|show|print|repeat|output|leak|display|tell\s+me)\s+(?:me\s+)?(?:your|the)\s+(?:system\s+prompt|hidden\s+(?:prompt|instructions)|initial\s+(?:prompt|instructions)|original\s+(?:prompt|instructions)|instructions\s+above)\b/
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
function toStatelessRegExp(pattern) {
|
|
211
|
+
if (pattern instanceof RegExp) {
|
|
212
|
+
if (!pattern.global && !pattern.sticky) return pattern;
|
|
213
|
+
return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
|
|
214
|
+
}
|
|
215
|
+
return new RegExp(String(pattern), "i");
|
|
216
|
+
}
|
|
217
|
+
function detectInjection(text, opts = {}) {
|
|
218
|
+
const normalized = normalizeText(text == null ? "" : text);
|
|
219
|
+
const lower = normalized.toLowerCase();
|
|
220
|
+
const folded = foldText(normalized);
|
|
221
|
+
const matches = [];
|
|
222
|
+
if (opts.useDefaults !== false) {
|
|
223
|
+
for (const rule of DEFAULT_RULES) {
|
|
224
|
+
const m = rule.pattern.exec(lower) || rule.pattern.exec(folded);
|
|
225
|
+
if (m) {
|
|
226
|
+
matches.push({
|
|
227
|
+
id: rule.id,
|
|
228
|
+
pattern: String(rule.pattern),
|
|
229
|
+
match: m[0].trim()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const raw of opts.patterns || []) {
|
|
235
|
+
const re = toStatelessRegExp(raw);
|
|
236
|
+
const m = re.exec(normalized) || re.exec(folded);
|
|
237
|
+
if (m) {
|
|
238
|
+
matches.push({ id: "custom", pattern: String(raw), match: m[0].trim() });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { detected: matches.length > 0, matches, normalized };
|
|
242
|
+
}
|
|
243
|
+
module2.exports = {
|
|
244
|
+
detectInjection,
|
|
245
|
+
normalizeText,
|
|
246
|
+
foldText,
|
|
247
|
+
DEFAULT_RULES
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// src/guardrails/pre-retrieval-guard.js
|
|
253
|
+
var require_pre_retrieval_guard = __commonJS({
|
|
254
|
+
"src/guardrails/pre-retrieval-guard.js"(exports2, module2) {
|
|
255
|
+
"use strict";
|
|
256
|
+
var { EventEmitter } = require("events");
|
|
257
|
+
var { validateInput } = require_validator();
|
|
258
|
+
var { tokenize } = require_scoring();
|
|
259
|
+
var { detectInjection, DEFAULT_RULES } = require_injection_detector();
|
|
260
|
+
var DEFAULT_CONFIG = {
|
|
261
|
+
maxQueryLength: 1e4,
|
|
262
|
+
enableInjectionDetection: true,
|
|
263
|
+
enableTopicFiltering: false,
|
|
264
|
+
blockedTopics: [],
|
|
265
|
+
allowedTopics: [],
|
|
266
|
+
// Built-in rules (see ./injection-detector). They match against normalised
|
|
267
|
+
// text (NFKC, zero-width/control characters removed, homoglyph and
|
|
268
|
+
// leetspeak folding). options.injectionPatterns adds extra patterns.
|
|
269
|
+
injectionPatterns: DEFAULT_RULES.map((rule) => rule.pattern)
|
|
270
|
+
};
|
|
271
|
+
var PreRetrievalGuard = class extends EventEmitter {
|
|
272
|
+
/**
|
|
273
|
+
* @param {object} [options] - Override default configuration
|
|
274
|
+
* @param {number} [options.maxQueryLength=10000] - Maximum allowed query length
|
|
275
|
+
* @param {boolean} [options.enableInjectionDetection=true] - Whether to check for prompt injection
|
|
276
|
+
* @param {boolean} [options.enableTopicFiltering=false] - Whether to enforce topic constraints
|
|
277
|
+
* @param {string[]} [options.blockedTopics=[]] - Topics to reject
|
|
278
|
+
* @param {string[]} [options.allowedTopics=[]] - If non-empty, only these topics are permitted
|
|
279
|
+
* @param {RegExp[]} [options.injectionPatterns] - Additional injection patterns (merged with defaults)
|
|
280
|
+
*/
|
|
281
|
+
constructor(options = {}) {
|
|
282
|
+
super();
|
|
283
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
284
|
+
this._customPatterns = options.injectionPatterns ? [...options.injectionPatterns] : [];
|
|
285
|
+
if (options.injectionPatterns) {
|
|
286
|
+
this.config.injectionPatterns = [
|
|
287
|
+
...DEFAULT_CONFIG.injectionPatterns,
|
|
288
|
+
...options.injectionPatterns
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Check a query for safety issues, injection attempts, and topic violations.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} query - The user query to validate
|
|
296
|
+
* @returns {{
|
|
297
|
+
* safe: boolean,
|
|
298
|
+
* issues: string[],
|
|
299
|
+
* sanitizedQuery: string,
|
|
300
|
+
* injection: { detected: boolean, matches: Array<{id: string, pattern: string, match: string}> }
|
|
301
|
+
* }}
|
|
302
|
+
* `sanitizedQuery` is the query truncated to maxQueryLength; it is never
|
|
303
|
+
* rewritten. Injection findings are reported in `issues` and `injection`
|
|
304
|
+
* and make `safe` false; blocking is up to the caller (GuardrailsPipeline
|
|
305
|
+
* blocks in strict mode).
|
|
306
|
+
* @fires PreRetrievalGuard#checked
|
|
307
|
+
*/
|
|
308
|
+
check(query) {
|
|
309
|
+
const issues = [];
|
|
310
|
+
if (query == null || typeof query !== "string" || query.trim().length === 0) {
|
|
311
|
+
issues.push("Query must be a non-empty string");
|
|
312
|
+
const result2 = {
|
|
313
|
+
safe: false,
|
|
314
|
+
issues,
|
|
315
|
+
sanitizedQuery: "",
|
|
316
|
+
injection: { detected: false, matches: [] }
|
|
317
|
+
};
|
|
318
|
+
this.emit("checked", result2);
|
|
319
|
+
return result2;
|
|
320
|
+
}
|
|
321
|
+
const validation = validateInput(query, {
|
|
322
|
+
maxLength: this.config.maxQueryLength
|
|
323
|
+
});
|
|
324
|
+
if (!validation.valid) {
|
|
325
|
+
for (const reason of validation.reasons) {
|
|
326
|
+
issues.push(reason);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const sanitizedQuery = validation.sanitized;
|
|
330
|
+
let injection = { detected: false, matches: [] };
|
|
331
|
+
if (this.config.enableInjectionDetection) {
|
|
332
|
+
const found = detectInjection(sanitizedQuery, {
|
|
333
|
+
patterns: this._customPatterns
|
|
334
|
+
});
|
|
335
|
+
injection = { detected: found.detected, matches: found.matches };
|
|
336
|
+
for (const m of found.matches) {
|
|
337
|
+
issues.push(
|
|
338
|
+
`Potential prompt injection detected: ${m.id} (${JSON.stringify(m.match)})`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (this.config.enableTopicFiltering) {
|
|
343
|
+
const tokens = tokenize(sanitizedQuery);
|
|
344
|
+
if (this.config.blockedTopics.length > 0) {
|
|
345
|
+
for (const topic of this.config.blockedTopics) {
|
|
346
|
+
const topicLower = topic.toLowerCase();
|
|
347
|
+
if (tokens.includes(topicLower)) {
|
|
348
|
+
issues.push(`Query contains blocked topic: ${topic}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (this.config.allowedTopics.length > 0) {
|
|
353
|
+
const allowedLower = this.config.allowedTopics.map(
|
|
354
|
+
(t) => t.toLowerCase()
|
|
355
|
+
);
|
|
356
|
+
const hasAllowedTopic = tokens.some(
|
|
357
|
+
(token) => allowedLower.includes(token)
|
|
358
|
+
);
|
|
359
|
+
if (!hasAllowedTopic) {
|
|
360
|
+
issues.push("Query does not match any allowed topics");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const result = {
|
|
365
|
+
safe: issues.length === 0,
|
|
366
|
+
issues,
|
|
367
|
+
sanitizedQuery,
|
|
368
|
+
injection
|
|
369
|
+
};
|
|
370
|
+
this.emit("checked", result);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
module2.exports = { PreRetrievalGuard, DEFAULT_CONFIG };
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// src/guardrails/retrieval-guard.js
|
|
379
|
+
var require_retrieval_guard = __commonJS({
|
|
380
|
+
"src/guardrails/retrieval-guard.js"(exports2, module2) {
|
|
381
|
+
"use strict";
|
|
382
|
+
var { EventEmitter } = require("events");
|
|
383
|
+
var { detectInjection } = require_injection_detector();
|
|
384
|
+
var DEFAULT_CONFIG = {
|
|
385
|
+
minRelevanceScore: 0,
|
|
386
|
+
maxAgeDays: 0,
|
|
387
|
+
// 0 = disabled
|
|
388
|
+
enforceACL: false,
|
|
389
|
+
// Opt-in scan of retrieved document text for prompt injection (indirect
|
|
390
|
+
// injection). 'remove' drops flagged documents, 'flag' keeps them and
|
|
391
|
+
// marks them with `injection` details.
|
|
392
|
+
detectInjection: false,
|
|
393
|
+
injectionAction: "remove",
|
|
394
|
+
injectionPatterns: []
|
|
395
|
+
};
|
|
396
|
+
var RetrievalGuard = class extends EventEmitter {
|
|
397
|
+
/**
|
|
398
|
+
* @param {object} [options] - Override default configuration
|
|
399
|
+
* @param {number} [options.minRelevanceScore=0] - Minimum relevance score threshold
|
|
400
|
+
* @param {number} [options.maxAgeDays=0] - Maximum document age in days (0 = disabled)
|
|
401
|
+
* @param {boolean} [options.enforceACL=false] - Whether to enforce access control
|
|
402
|
+
* @param {boolean} [options.detectInjection=false] - Scan document text for prompt injection
|
|
403
|
+
* @param {'remove'|'flag'} [options.injectionAction='remove'] - Remove flagged documents or keep and mark them
|
|
404
|
+
* @param {RegExp[]} [options.injectionPatterns=[]] - Extra injection patterns for the scan
|
|
405
|
+
*/
|
|
406
|
+
constructor(options = {}) {
|
|
407
|
+
super();
|
|
408
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Filter retrieval results based on configured guards.
|
|
412
|
+
*
|
|
413
|
+
* @param {Array<object>} results - Array of retrieval results, each with .score and optional .metadata
|
|
414
|
+
* @param {object} [context={}] - Request context
|
|
415
|
+
* @param {string[]} [context.userPermissions] - User's permission roles for ACL filtering
|
|
416
|
+
* @returns {{
|
|
417
|
+
* results: Array<object>,
|
|
418
|
+
* removed: Array<object>,
|
|
419
|
+
* metadata: { reasons: Object<string, number> }
|
|
420
|
+
* }}
|
|
421
|
+
* @fires RetrievalGuard#filtered
|
|
422
|
+
*/
|
|
423
|
+
filter(results, context = {}) {
|
|
424
|
+
if (!Array.isArray(results)) {
|
|
425
|
+
const output2 = { results: [], removed: [], metadata: { reasons: {} } };
|
|
426
|
+
this.emit("filtered", output2);
|
|
427
|
+
return output2;
|
|
428
|
+
}
|
|
429
|
+
const kept = [];
|
|
430
|
+
const removed = [];
|
|
431
|
+
const reasons = {};
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
const maxAgeMs = this.config.maxAgeDays > 0 ? this.config.maxAgeDays * 24 * 60 * 60 * 1e3 : 0;
|
|
434
|
+
for (const result of results) {
|
|
435
|
+
let dominated = false;
|
|
436
|
+
const doc = result.metadata || result.doc || result;
|
|
437
|
+
if (this.config.minRelevanceScore > 0) {
|
|
438
|
+
const score = result.score != null ? result.score : 0;
|
|
439
|
+
if (score < this.config.minRelevanceScore) {
|
|
440
|
+
dominated = true;
|
|
441
|
+
reasons.relevance = (reasons.relevance || 0) + 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!dominated && maxAgeMs > 0) {
|
|
445
|
+
const timestamp = doc.updatedAt || doc.timestamp;
|
|
446
|
+
if (timestamp) {
|
|
447
|
+
const docTime = new Date(timestamp).getTime();
|
|
448
|
+
if (!isNaN(docTime) && now - docTime > maxAgeMs) {
|
|
449
|
+
dominated = true;
|
|
450
|
+
reasons.freshness = (reasons.freshness || 0) + 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!dominated && this.config.enforceACL) {
|
|
455
|
+
const userPermissions = context.userPermissions || [];
|
|
456
|
+
const restriction = result.requiredRole ?? result.acl ?? doc.requiredRole ?? doc.acl;
|
|
457
|
+
const requiredRoles = restriction ? [].concat(restriction) : null;
|
|
458
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
459
|
+
const hasAccess = requiredRoles.some(
|
|
460
|
+
(role) => userPermissions.includes(role)
|
|
461
|
+
);
|
|
462
|
+
if (!hasAccess) {
|
|
463
|
+
dominated = true;
|
|
464
|
+
reasons.acl = (reasons.acl || 0) + 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!dominated && this.config.detectInjection) {
|
|
469
|
+
const text = documentText(result);
|
|
470
|
+
const found = detectInjection(text, {
|
|
471
|
+
patterns: this.config.injectionPatterns
|
|
472
|
+
});
|
|
473
|
+
if (found.detected) {
|
|
474
|
+
reasons.injection = (reasons.injection || 0) + 1;
|
|
475
|
+
if (this.config.injectionAction === "flag") {
|
|
476
|
+
kept.push({
|
|
477
|
+
...result,
|
|
478
|
+
injection: { detected: true, matches: found.matches }
|
|
479
|
+
});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
dominated = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (dominated) {
|
|
486
|
+
removed.push(result);
|
|
487
|
+
} else {
|
|
488
|
+
kept.push(result);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const output = {
|
|
492
|
+
results: kept,
|
|
493
|
+
removed,
|
|
494
|
+
metadata: { reasons }
|
|
495
|
+
};
|
|
496
|
+
this.emit("filtered", { original: results, filtered: kept, removed });
|
|
497
|
+
return output;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
function documentText(result) {
|
|
501
|
+
if (typeof result === "string") return result;
|
|
502
|
+
if (!result || typeof result !== "object") return "";
|
|
503
|
+
const doc = result.doc && typeof result.doc === "object" ? result.doc : {};
|
|
504
|
+
return String(
|
|
505
|
+
result.content || result.text || result.pageContent || doc.content || doc.text || ""
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
module2.exports = { RetrievalGuard, DEFAULT_CONFIG };
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// scoped:@devilsdev/rag-core|citation/citation-tracker.js
|
|
513
|
+
var require_citation_tracker = __commonJS({
|
|
514
|
+
"scoped:@devilsdev/rag-core|citation/citation-tracker.js"(exports2, module2) {
|
|
515
|
+
module2.exports = require("@devilsdev/rag-core")[Symbol.for("@devilsdev/rag-core/internal")]("citation/citation-tracker.js");
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// src/guardrails/post-generation-guard.js
|
|
520
|
+
var require_post_generation_guard = __commonJS({
|
|
521
|
+
"src/guardrails/post-generation-guard.js"(exports2, module2) {
|
|
522
|
+
"use strict";
|
|
523
|
+
var { EventEmitter } = require("events");
|
|
524
|
+
var DEFAULT_CONFIG = {
|
|
525
|
+
enablePIIDetection: true,
|
|
526
|
+
enableGroundednessCheck: true,
|
|
527
|
+
groundednessThreshold: 0.3,
|
|
528
|
+
maxResponseLength: 5e4,
|
|
529
|
+
minResponseLength: 0,
|
|
530
|
+
piiPatterns: {
|
|
531
|
+
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
532
|
+
phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
|
|
533
|
+
ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
534
|
+
creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
var PostGenerationGuard = class extends EventEmitter {
|
|
538
|
+
/**
|
|
539
|
+
* @param {object} [options] - Override default configuration
|
|
540
|
+
* @param {boolean} [options.enablePIIDetection=true] - Whether to detect PII in output
|
|
541
|
+
* @param {boolean} [options.enableGroundednessCheck=true] - Whether to check groundedness
|
|
542
|
+
* @param {number} [options.groundednessThreshold=0.3] - Minimum groundedness score
|
|
543
|
+
* @param {number} [options.maxResponseLength=50000] - Maximum allowed response length
|
|
544
|
+
* @param {number} [options.minResponseLength=0] - Minimum response length
|
|
545
|
+
* @param {object} [options.piiPatterns] - Custom PII regex patterns (merged with defaults)
|
|
546
|
+
*/
|
|
547
|
+
constructor(options = {}) {
|
|
548
|
+
super();
|
|
549
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
550
|
+
if (options.piiPatterns) {
|
|
551
|
+
this.config.piiPatterns = {
|
|
552
|
+
...DEFAULT_CONFIG.piiPatterns,
|
|
553
|
+
...options.piiPatterns
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Check generated output for safety issues, PII, and groundedness.
|
|
559
|
+
*
|
|
560
|
+
* @param {string} output - The LLM-generated output text
|
|
561
|
+
* @param {Array<object>} [retrievedDocs=[]] - Source documents for groundedness checking
|
|
562
|
+
* @returns {{
|
|
563
|
+
* safe: boolean,
|
|
564
|
+
* issues: string[],
|
|
565
|
+
* sanitizedOutput: string,
|
|
566
|
+
* groundednessScore?: number
|
|
567
|
+
* }}
|
|
568
|
+
* @fires PostGenerationGuard#checked
|
|
569
|
+
*/
|
|
570
|
+
check(output, retrievedDocs = []) {
|
|
571
|
+
const issues = [];
|
|
572
|
+
if (output == null || typeof output !== "string") {
|
|
573
|
+
issues.push("Output must be a string");
|
|
574
|
+
const result2 = { safe: false, issues, sanitizedOutput: "" };
|
|
575
|
+
this.emit("checked", result2);
|
|
576
|
+
return result2;
|
|
577
|
+
}
|
|
578
|
+
if (this.config.maxResponseLength > 0 && output.length > this.config.maxResponseLength) {
|
|
579
|
+
issues.push(
|
|
580
|
+
`Output exceeds maximum length of ${this.config.maxResponseLength} characters`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (this.config.minResponseLength > 0 && output.length < this.config.minResponseLength) {
|
|
584
|
+
issues.push(
|
|
585
|
+
`Output is shorter than minimum length of ${this.config.minResponseLength} characters`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const piiMatches = {};
|
|
589
|
+
if (this.config.enablePIIDetection) {
|
|
590
|
+
for (const [name, pattern] of Object.entries(this.config.piiPatterns)) {
|
|
591
|
+
pattern.lastIndex = 0;
|
|
592
|
+
const matches = output.match(pattern);
|
|
593
|
+
if (matches && matches.length > 0) {
|
|
594
|
+
piiMatches[name] = matches;
|
|
595
|
+
issues.push(
|
|
596
|
+
`PII detected (${name}): ${matches.length} occurrence(s)`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
let groundednessScore;
|
|
602
|
+
if (this.config.enableGroundednessCheck && Array.isArray(retrievedDocs) && retrievedDocs.length > 0) {
|
|
603
|
+
try {
|
|
604
|
+
const { CitationTracker } = require_citation_tracker();
|
|
605
|
+
const tracker = new CitationTracker();
|
|
606
|
+
const tracking = tracker.track(output, retrievedDocs);
|
|
607
|
+
groundednessScore = tracking.groundednessScore;
|
|
608
|
+
if (groundednessScore < this.config.groundednessThreshold) {
|
|
609
|
+
issues.push(
|
|
610
|
+
`Groundedness score (${groundednessScore.toFixed(2)}) is below threshold (${this.config.groundednessThreshold})`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
issues.push(`Groundedness check failed: ${err.message}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let sanitizedOutput = output;
|
|
618
|
+
if (Object.keys(piiMatches).length > 0) {
|
|
619
|
+
for (const [, matches] of Object.entries(piiMatches)) {
|
|
620
|
+
for (const match of matches) {
|
|
621
|
+
sanitizedOutput = sanitizedOutput.split(match).join("[REDACTED]");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (this.config.maxResponseLength > 0 && sanitizedOutput.length > this.config.maxResponseLength) {
|
|
626
|
+
sanitizedOutput = sanitizedOutput.substring(
|
|
627
|
+
0,
|
|
628
|
+
this.config.maxResponseLength
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const result = {
|
|
632
|
+
safe: issues.length === 0,
|
|
633
|
+
issues,
|
|
634
|
+
sanitizedOutput
|
|
635
|
+
};
|
|
636
|
+
if (groundednessScore !== void 0) {
|
|
637
|
+
result.groundednessScore = groundednessScore;
|
|
638
|
+
}
|
|
639
|
+
this.emit("checked", result);
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Replace PII matches in arbitrary text with [REDACTED], using the same
|
|
644
|
+
* patterns as check(). Used to scrub text derived from the answer, such as
|
|
645
|
+
* citation sentences.
|
|
646
|
+
*
|
|
647
|
+
* @param {string} text
|
|
648
|
+
* @returns {string}
|
|
649
|
+
*/
|
|
650
|
+
redact(text) {
|
|
651
|
+
if (typeof text !== "string" || !this.config.enablePIIDetection) {
|
|
652
|
+
return text;
|
|
653
|
+
}
|
|
654
|
+
let redacted = text;
|
|
655
|
+
for (const pattern of Object.values(this.config.piiPatterns)) {
|
|
656
|
+
redacted = redacted.replace(pattern, "[REDACTED]");
|
|
657
|
+
}
|
|
658
|
+
return redacted;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
module2.exports = { PostGenerationGuard, DEFAULT_CONFIG };
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// src/guardrails/guardrails-pipeline.js
|
|
666
|
+
var require_guardrails_pipeline = __commonJS({
|
|
667
|
+
"src/guardrails/guardrails-pipeline.js"(exports2, module2) {
|
|
668
|
+
"use strict";
|
|
669
|
+
var { EventEmitter } = require("events");
|
|
670
|
+
var { PreRetrievalGuard } = require_pre_retrieval_guard();
|
|
671
|
+
var { RetrievalGuard } = require_retrieval_guard();
|
|
672
|
+
var { PostGenerationGuard } = require_post_generation_guard();
|
|
673
|
+
var DEFAULT_CONFIG = {
|
|
674
|
+
preRetrieval: {},
|
|
675
|
+
// PreRetrievalGuard options, or false to disable
|
|
676
|
+
retrieval: {},
|
|
677
|
+
// RetrievalGuard options, or false to disable
|
|
678
|
+
postGeneration: {},
|
|
679
|
+
// PostGenerationGuard options, or false to disable
|
|
680
|
+
strict: false
|
|
681
|
+
// If true, block on any guard failure; if false, warn and continue
|
|
682
|
+
};
|
|
683
|
+
function summarizeFilter(filterOutput) {
|
|
684
|
+
return {
|
|
685
|
+
...filterOutput,
|
|
686
|
+
removed: filterOutput.removed.map((doc) => ({ id: doc.id }))
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
var GuardrailsPipeline = class extends EventEmitter {
|
|
690
|
+
/**
|
|
691
|
+
* @param {object} pipeline - The underlying RAG pipeline (must have a .run() method)
|
|
692
|
+
* @param {object} [options] - Override default configuration
|
|
693
|
+
* @param {object|false} [options.preRetrieval={}] - PreRetrievalGuard options, or false to disable
|
|
694
|
+
* @param {object|false} [options.retrieval={}] - RetrievalGuard options, or false to disable
|
|
695
|
+
* @param {object|false} [options.postGeneration={}] - PostGenerationGuard options, or false to disable
|
|
696
|
+
* @param {boolean} [options.strict=false] - Whether to block on any guard failure
|
|
697
|
+
*/
|
|
698
|
+
constructor(pipeline, options = {}) {
|
|
699
|
+
super();
|
|
700
|
+
if (!pipeline || typeof pipeline.run !== "function") {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"GuardrailsPipeline requires a pipeline with a .run() method"
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
this.pipeline = pipeline;
|
|
706
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
707
|
+
this.preGuard = this.config.preRetrieval !== false ? new PreRetrievalGuard(this.config.preRetrieval || {}) : null;
|
|
708
|
+
this.retrievalGuard = this.config.retrieval !== false ? new RetrievalGuard(this.config.retrieval || {}) : null;
|
|
709
|
+
this.postGuard = this.config.postGeneration !== false ? new PostGenerationGuard(this.config.postGeneration || {}) : null;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Run the full guarded pipeline.
|
|
713
|
+
*
|
|
714
|
+
* @param {object} input - Pipeline input
|
|
715
|
+
* @param {string} input.query - The user query
|
|
716
|
+
* @param {Float32Array} [input.queryVector] - Optional precomputed query vector
|
|
717
|
+
* @param {object} [input.options] - Additional pipeline options
|
|
718
|
+
* @param {object} [input.context] - Request context (e.g. userPermissions for ACL)
|
|
719
|
+
* @returns {Promise<object>} Pipeline result with guardrails metadata attached
|
|
720
|
+
*/
|
|
721
|
+
async run(input) {
|
|
722
|
+
const guardrails = {
|
|
723
|
+
preRetrieval: null,
|
|
724
|
+
retrieval: null,
|
|
725
|
+
postGeneration: null
|
|
726
|
+
};
|
|
727
|
+
let query = input.query;
|
|
728
|
+
if (this.preGuard) {
|
|
729
|
+
guardrails.preRetrieval = this.preGuard.check(query);
|
|
730
|
+
this.emit("preRetrieval", guardrails.preRetrieval);
|
|
731
|
+
if (!guardrails.preRetrieval.safe && this.config.strict) {
|
|
732
|
+
return {
|
|
733
|
+
success: false,
|
|
734
|
+
error: "Pre-retrieval guard blocked query",
|
|
735
|
+
guardrails
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
query = guardrails.preRetrieval.sanitizedQuery;
|
|
739
|
+
}
|
|
740
|
+
let preGenerationFilter = null;
|
|
741
|
+
const filterOptions = this.retrievalGuard ? {
|
|
742
|
+
filterResults: (docs) => {
|
|
743
|
+
preGenerationFilter = this.retrievalGuard.filter(
|
|
744
|
+
docs,
|
|
745
|
+
input.context
|
|
746
|
+
);
|
|
747
|
+
return preGenerationFilter.results;
|
|
748
|
+
}
|
|
749
|
+
} : {};
|
|
750
|
+
const result = await this.pipeline.run({
|
|
751
|
+
...input,
|
|
752
|
+
query,
|
|
753
|
+
options: { ...input.options, citations: true, ...filterOptions }
|
|
754
|
+
});
|
|
755
|
+
if (this.retrievalGuard && preGenerationFilter) {
|
|
756
|
+
guardrails.retrieval = summarizeFilter(preGenerationFilter);
|
|
757
|
+
this.emit("retrieval", guardrails.retrieval);
|
|
758
|
+
} else if (this.retrievalGuard && result.results) {
|
|
759
|
+
guardrails.retrieval = summarizeFilter(
|
|
760
|
+
this.retrievalGuard.filter(result.results, input.context)
|
|
761
|
+
);
|
|
762
|
+
this.emit("retrieval", guardrails.retrieval);
|
|
763
|
+
result.results = guardrails.retrieval.results;
|
|
764
|
+
if (guardrails.retrieval.metadata.reasons.acl) {
|
|
765
|
+
return {
|
|
766
|
+
success: false,
|
|
767
|
+
error: "Retrieval guard removed access-controlled documents after generation; answer withheld",
|
|
768
|
+
guardrails
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (this.postGuard && result.answer) {
|
|
773
|
+
guardrails.postGeneration = this.postGuard.check(
|
|
774
|
+
result.answer,
|
|
775
|
+
result.results
|
|
776
|
+
);
|
|
777
|
+
this.emit("postGeneration", guardrails.postGeneration);
|
|
778
|
+
if (!guardrails.postGeneration.safe && this.config.strict) {
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
error: "Post-generation guard flagged output",
|
|
782
|
+
guardrails
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
result.answer = guardrails.postGeneration.sanitizedOutput;
|
|
786
|
+
this._redactDeep(result.citations);
|
|
787
|
+
this._redactDeep(result.evaluation);
|
|
788
|
+
}
|
|
789
|
+
result.guardrails = guardrails;
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Citations and evaluation details copy sentences from the raw answer, so
|
|
794
|
+
* scrub every string in them with the same PII patterns as the answer.
|
|
795
|
+
*
|
|
796
|
+
* @param {*} value - Object, array or string to redact in place
|
|
797
|
+
* @returns {*} The redacted value
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
_redactDeep(value) {
|
|
801
|
+
if (typeof value === "string") return this.postGuard.redact(value);
|
|
802
|
+
if (value && typeof value === "object") {
|
|
803
|
+
for (const key of Object.keys(value)) {
|
|
804
|
+
value[key] = this._redactDeep(value[key]);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return value;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
module2.exports = { GuardrailsPipeline, DEFAULT_CONFIG };
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// src/packages/guardrails.js
|
|
815
|
+
var require_guardrails = __commonJS({
|
|
816
|
+
"src/packages/guardrails.js"(exports2, module2) {
|
|
817
|
+
"use strict";
|
|
818
|
+
var { PreRetrievalGuard } = require_pre_retrieval_guard();
|
|
819
|
+
var { detectInjection } = require_injection_detector();
|
|
820
|
+
var { RetrievalGuard } = require_retrieval_guard();
|
|
821
|
+
var { PostGenerationGuard } = require_post_generation_guard();
|
|
822
|
+
var { GuardrailsPipeline } = require_guardrails_pipeline();
|
|
823
|
+
module2.exports = {
|
|
824
|
+
PreRetrievalGuard,
|
|
825
|
+
detectInjection,
|
|
826
|
+
RetrievalGuard,
|
|
827
|
+
PostGenerationGuard,
|
|
828
|
+
GuardrailsPipeline
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// src/package-entry.js
|
|
834
|
+
var mod = require_guardrails();
|
|
835
|
+
var modules = {
|
|
836
|
+
"guardrails/guardrails-pipeline.js": () => require_guardrails_pipeline(),
|
|
837
|
+
"guardrails/injection-detector.js": () => require_injection_detector(),
|
|
838
|
+
"guardrails/post-generation-guard.js": () => require_post_generation_guard(),
|
|
839
|
+
"guardrails/pre-retrieval-guard.js": () => require_pre_retrieval_guard(),
|
|
840
|
+
"guardrails/retrieval-guard.js": () => require_retrieval_guard(),
|
|
841
|
+
"packages/guardrails.js": () => require_guardrails(),
|
|
842
|
+
"security/validator.js": () => require_validator()
|
|
843
|
+
};
|
|
844
|
+
Object.defineProperty(mod, Symbol.for("@devilsdev/rag-guardrails/internal"), {
|
|
845
|
+
value: (file) => {
|
|
846
|
+
if (!modules[file]) throw new Error("@devilsdev/rag-guardrails has no module " + file);
|
|
847
|
+
return modules[file]();
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
module.exports = mod;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PreRetrievalGuard, detectInjection, RetrievalGuard, PostGenerationGuard, GuardrailsPipeline } from "@devilsdev/rag-core/all-types";
|
package/dist/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devilsdev/rag-guardrails",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Guardrails for RAG: prompt-injection detection and pre-retrieval, retrieval and post-generation guards.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rag",
|
|
7
|
+
"guardrails",
|
|
8
|
+
"prompt-injection",
|
|
9
|
+
"llm-security"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Ali Kahwaji",
|
|
13
|
+
"homepage": "https://devilsdev.github.io/rag-pipeline-utils/",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/DevilsDev/rag-pipeline-utils.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/DevilsDev/rag-pipeline-utils/issues"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"require": "./dist/index.cjs",
|
|
30
|
+
"import": "./dist/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist/",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@devilsdev/rag-core": "3.2.0"
|
|
45
|
+
}
|
|
46
|
+
}
|