@dudousxd/nestjs-agent-core 0.15.5 → 0.17.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/README.md +68 -0
- package/dist/guardrails/index.cjs +2519 -0
- package/dist/guardrails/index.cjs.map +1 -0
- package/dist/guardrails/index.d.cts +660 -0
- package/dist/guardrails/index.d.ts +660 -0
- package/dist/guardrails/index.js +2445 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/index.cjs +27 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1013
- package/dist/index.d.ts +81 -1013
- package/dist/index.js +25 -25
- package/dist/index.js.map +1 -1
- package/dist/tool-CL9oEytW.d.cts +1014 -0
- package/dist/tool-CL9oEytW.d.ts +1014 -0
- package/package.json +14 -3
|
@@ -0,0 +1,2519 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/guardrails/index.ts
|
|
22
|
+
var guardrails_exports = {};
|
|
23
|
+
__export(guardrails_exports, {
|
|
24
|
+
ACTION_SEVERITY: () => ACTION_SEVERITY,
|
|
25
|
+
DEFAULT_APPROVAL_MESSAGE: () => DEFAULT_APPROVAL_MESSAGE,
|
|
26
|
+
DEFAULT_MESSAGES: () => DEFAULT_MESSAGES,
|
|
27
|
+
GUARDRAIL_STAGES: () => GUARDRAIL_STAGES,
|
|
28
|
+
GuardrailBlockedError: () => GuardrailBlockedError,
|
|
29
|
+
Guardrails: () => Guardrails,
|
|
30
|
+
INJECTION_REPLACEMENT: () => INJECTION_REPLACEMENT,
|
|
31
|
+
INJECTION_SIGNALS: () => INJECTION_SIGNALS,
|
|
32
|
+
InMemoryVaultStore: () => InMemoryVaultStore,
|
|
33
|
+
PII_TYPES: () => PII_TYPES,
|
|
34
|
+
SECRET_TYPES: () => SECRET_TYPES,
|
|
35
|
+
StreamGuard: () => StreamGuard,
|
|
36
|
+
Vault: () => Vault,
|
|
37
|
+
applyRedactions: () => applyRedactions,
|
|
38
|
+
cardBrand: () => cardBrand,
|
|
39
|
+
cnpjValid: () => cnpjValid,
|
|
40
|
+
compileGuardPattern: () => compileGuardPattern,
|
|
41
|
+
cpfValid: () => cpfValid,
|
|
42
|
+
createGuardrails: () => createGuardrails,
|
|
43
|
+
decodeTagChars: () => decodeTagChars,
|
|
44
|
+
detectKeywords: () => detectKeywords,
|
|
45
|
+
detectPii: () => detectPii,
|
|
46
|
+
detectRegex: () => detectRegex,
|
|
47
|
+
detectSecrets: () => detectSecrets,
|
|
48
|
+
entropy: () => entropy,
|
|
49
|
+
fold: () => fold,
|
|
50
|
+
globToRegExp: () => globToRegExp,
|
|
51
|
+
ibanValid: () => ibanValid,
|
|
52
|
+
isIPv4: () => isIPv4,
|
|
53
|
+
jsonSlots: () => jsonSlots,
|
|
54
|
+
labelFor: () => labelFor,
|
|
55
|
+
luhnValid: () => luhnValid,
|
|
56
|
+
matchesAny: () => matchesAny,
|
|
57
|
+
orderRules: () => orderRules,
|
|
58
|
+
phoneValid: () => phoneValid,
|
|
59
|
+
refuseResponse: () => refuseResponse,
|
|
60
|
+
requestSlots: () => requestSlots,
|
|
61
|
+
resolveOverlaps: () => resolveOverlaps,
|
|
62
|
+
responseSlots: () => responseSlots,
|
|
63
|
+
ruleApplies: () => ruleApplies,
|
|
64
|
+
runDetector: () => runDetector,
|
|
65
|
+
scan: () => scan,
|
|
66
|
+
scoreInjection: () => scoreInjection,
|
|
67
|
+
scoreToolText: () => scoreToolText,
|
|
68
|
+
screenToolDefinition: () => screenToolDefinition,
|
|
69
|
+
shorthandRules: () => shorthandRules,
|
|
70
|
+
spanLocator: () => spanLocator,
|
|
71
|
+
ssnValid: () => ssnValid,
|
|
72
|
+
toolResultSlots: () => toolResultSlots,
|
|
73
|
+
toolText: () => toolText
|
|
74
|
+
});
|
|
75
|
+
module.exports = __toCommonJS(guardrails_exports);
|
|
76
|
+
|
|
77
|
+
// src/guardrails/types.ts
|
|
78
|
+
var GUARDRAIL_STAGES = [
|
|
79
|
+
"llm_request",
|
|
80
|
+
"llm_response",
|
|
81
|
+
"tool_args",
|
|
82
|
+
"tool_result",
|
|
83
|
+
"tool_description"
|
|
84
|
+
];
|
|
85
|
+
var ACTION_SEVERITY = {
|
|
86
|
+
allow: 0,
|
|
87
|
+
log: 1,
|
|
88
|
+
redact: 2,
|
|
89
|
+
approve: 3,
|
|
90
|
+
block: 4
|
|
91
|
+
};
|
|
92
|
+
var PII_TYPES = [
|
|
93
|
+
"email",
|
|
94
|
+
"phone",
|
|
95
|
+
"cpf",
|
|
96
|
+
"cnpj",
|
|
97
|
+
"ssn",
|
|
98
|
+
"credit_card",
|
|
99
|
+
"iban",
|
|
100
|
+
"ip_address"
|
|
101
|
+
];
|
|
102
|
+
var SECRET_TYPES = [
|
|
103
|
+
"openai_key",
|
|
104
|
+
"anthropic_key",
|
|
105
|
+
"aws_access_key",
|
|
106
|
+
"aws_secret_key",
|
|
107
|
+
"github_token",
|
|
108
|
+
"gitlab_token",
|
|
109
|
+
"slack_token",
|
|
110
|
+
"google_api_key",
|
|
111
|
+
"stripe_key",
|
|
112
|
+
"jwt",
|
|
113
|
+
"bearer_token",
|
|
114
|
+
"private_key",
|
|
115
|
+
"generic_secret"
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// src/guardrails/detectors/pii.ts
|
|
119
|
+
function isIPv4(raw) {
|
|
120
|
+
const parts = raw.split(".");
|
|
121
|
+
return parts.length === 4 && parts.every((p) => /^(?:0|[1-9]\d{0,2})$/.test(p) && Number(p) <= 255);
|
|
122
|
+
}
|
|
123
|
+
__name(isIPv4, "isIPv4");
|
|
124
|
+
function luhnValid(digits) {
|
|
125
|
+
if (!/^\d+$/.test(digits)) return false;
|
|
126
|
+
let sum = 0;
|
|
127
|
+
let double = false;
|
|
128
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
129
|
+
let d = digits.charCodeAt(i) - 48;
|
|
130
|
+
if (double) {
|
|
131
|
+
d *= 2;
|
|
132
|
+
if (d > 9) d -= 9;
|
|
133
|
+
}
|
|
134
|
+
sum += d;
|
|
135
|
+
double = !double;
|
|
136
|
+
}
|
|
137
|
+
return sum % 10 === 0;
|
|
138
|
+
}
|
|
139
|
+
__name(luhnValid, "luhnValid");
|
|
140
|
+
function cardBrand(digits) {
|
|
141
|
+
const p2 = Number(digits.slice(0, 2));
|
|
142
|
+
const p3 = Number(digits.slice(0, 3));
|
|
143
|
+
const p4 = Number(digits.slice(0, 4));
|
|
144
|
+
const p6 = Number(digits.slice(0, 6));
|
|
145
|
+
const len = digits.length;
|
|
146
|
+
if ([
|
|
147
|
+
401178,
|
|
148
|
+
401179,
|
|
149
|
+
431274,
|
|
150
|
+
438935,
|
|
151
|
+
451416,
|
|
152
|
+
457393,
|
|
153
|
+
457631,
|
|
154
|
+
457632,
|
|
155
|
+
504175,
|
|
156
|
+
627780,
|
|
157
|
+
636297,
|
|
158
|
+
636368
|
|
159
|
+
].includes(p6)) return "elo";
|
|
160
|
+
if (p6 >= 506699 && p6 <= 506778 || p6 >= 509e3 && p6 <= 509999 || p6 >= 650031 && p6 <= 650051) return "elo";
|
|
161
|
+
if (p6 === 606282 || p4 === 3841) return "hipercard";
|
|
162
|
+
if (digits.startsWith("4") && (len === 13 || len === 16 || len === 19)) return "visa";
|
|
163
|
+
if ((p2 >= 51 && p2 <= 55 || p4 >= 2221 && p4 <= 2720) && len === 16) return "mastercard";
|
|
164
|
+
if ((p2 === 34 || p2 === 37) && len === 15) return "amex";
|
|
165
|
+
if ((p4 === 6011 || p2 === 65 || p3 >= 644 && p3 <= 649) && len >= 16) return "discover";
|
|
166
|
+
if ((p2 === 36 || p2 === 38 || p3 >= 300 && p3 <= 305) && len >= 14) return "diners";
|
|
167
|
+
if (p4 >= 3528 && p4 <= 3589 && len >= 16) return "jcb";
|
|
168
|
+
if ((p2 === 62 || p2 === 81) && len >= 16) return "unionpay";
|
|
169
|
+
if ((p4 === 5018 || p4 === 5020 || p4 === 5038 || p4 === 6304 || p4 === 6759 || p4 === 6761) && len >= 12) return "maestro";
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
__name(cardBrand, "cardBrand");
|
|
173
|
+
function cpfValid(raw) {
|
|
174
|
+
const d = raw.replace(/\D/g, "");
|
|
175
|
+
if (d.length !== 11 || /^(\d)\1{10}$/.test(d)) return false;
|
|
176
|
+
const check = /* @__PURE__ */ __name((len) => {
|
|
177
|
+
let sum = 0;
|
|
178
|
+
for (let i = 0; i < len; i++) sum += (d.charCodeAt(i) - 48) * (len + 1 - i);
|
|
179
|
+
const r = sum * 10 % 11;
|
|
180
|
+
return r === 10 ? 0 : r;
|
|
181
|
+
}, "check");
|
|
182
|
+
return check(9) === d.charCodeAt(9) - 48 && check(10) === d.charCodeAt(10) - 48;
|
|
183
|
+
}
|
|
184
|
+
__name(cpfValid, "cpfValid");
|
|
185
|
+
function cnpjValid(raw) {
|
|
186
|
+
const c = raw.replace(/[.\-/\s]/g, "").toUpperCase();
|
|
187
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(c) || /^(\d)\1{13}$/.test(c)) return false;
|
|
188
|
+
const value = /* @__PURE__ */ __name((i) => c.charCodeAt(i) - 48, "value");
|
|
189
|
+
const check = /* @__PURE__ */ __name((len) => {
|
|
190
|
+
const weights = len === 12 ? [
|
|
191
|
+
5,
|
|
192
|
+
4,
|
|
193
|
+
3,
|
|
194
|
+
2,
|
|
195
|
+
9,
|
|
196
|
+
8,
|
|
197
|
+
7,
|
|
198
|
+
6,
|
|
199
|
+
5,
|
|
200
|
+
4,
|
|
201
|
+
3,
|
|
202
|
+
2
|
|
203
|
+
] : [
|
|
204
|
+
6,
|
|
205
|
+
5,
|
|
206
|
+
4,
|
|
207
|
+
3,
|
|
208
|
+
2,
|
|
209
|
+
9,
|
|
210
|
+
8,
|
|
211
|
+
7,
|
|
212
|
+
6,
|
|
213
|
+
5,
|
|
214
|
+
4,
|
|
215
|
+
3,
|
|
216
|
+
2
|
|
217
|
+
];
|
|
218
|
+
let sum = 0;
|
|
219
|
+
for (let i = 0; i < len; i++) sum += value(i) * (weights[i] ?? 0);
|
|
220
|
+
const r = sum % 11;
|
|
221
|
+
return r < 2 ? 0 : 11 - r;
|
|
222
|
+
}, "check");
|
|
223
|
+
return check(12) === value(12) && check(13) === value(13);
|
|
224
|
+
}
|
|
225
|
+
__name(cnpjValid, "cnpjValid");
|
|
226
|
+
function ssnValid(raw) {
|
|
227
|
+
const m = /^(\d{3})[- ](\d{2})[- ](\d{4})$/.exec(raw);
|
|
228
|
+
if (!m) return false;
|
|
229
|
+
const area = m[1] ?? "";
|
|
230
|
+
const group = m[2] ?? "";
|
|
231
|
+
const serial = m[3] ?? "";
|
|
232
|
+
if (area === "000" || area === "666" || area.startsWith("9")) return false;
|
|
233
|
+
if (group === "00" || serial === "0000") return false;
|
|
234
|
+
if ([
|
|
235
|
+
"078-05-1120",
|
|
236
|
+
"219-09-9999",
|
|
237
|
+
"123-45-6789"
|
|
238
|
+
].includes(`${area}-${group}-${serial}`)) return false;
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
__name(ssnValid, "ssnValid");
|
|
242
|
+
var IBAN_LENGTHS = {
|
|
243
|
+
AD: 24,
|
|
244
|
+
AE: 23,
|
|
245
|
+
AT: 20,
|
|
246
|
+
BE: 16,
|
|
247
|
+
BG: 22,
|
|
248
|
+
BH: 22,
|
|
249
|
+
BR: 29,
|
|
250
|
+
CH: 21,
|
|
251
|
+
CR: 22,
|
|
252
|
+
CY: 28,
|
|
253
|
+
CZ: 24,
|
|
254
|
+
DE: 22,
|
|
255
|
+
DK: 18,
|
|
256
|
+
DO: 28,
|
|
257
|
+
EE: 20,
|
|
258
|
+
ES: 24,
|
|
259
|
+
FI: 18,
|
|
260
|
+
FO: 18,
|
|
261
|
+
FR: 27,
|
|
262
|
+
GB: 22,
|
|
263
|
+
GI: 23,
|
|
264
|
+
GL: 18,
|
|
265
|
+
GR: 27,
|
|
266
|
+
GT: 28,
|
|
267
|
+
HR: 21,
|
|
268
|
+
HU: 28,
|
|
269
|
+
IE: 22,
|
|
270
|
+
IL: 23,
|
|
271
|
+
IS: 26,
|
|
272
|
+
IT: 27,
|
|
273
|
+
JO: 30,
|
|
274
|
+
KW: 30,
|
|
275
|
+
KZ: 20,
|
|
276
|
+
LB: 28,
|
|
277
|
+
LI: 21,
|
|
278
|
+
LT: 20,
|
|
279
|
+
LU: 20,
|
|
280
|
+
LV: 21,
|
|
281
|
+
MC: 27,
|
|
282
|
+
MD: 24,
|
|
283
|
+
ME: 22,
|
|
284
|
+
MK: 19,
|
|
285
|
+
MR: 27,
|
|
286
|
+
MT: 31,
|
|
287
|
+
MU: 30,
|
|
288
|
+
NL: 18,
|
|
289
|
+
NO: 15,
|
|
290
|
+
PK: 24,
|
|
291
|
+
PL: 28,
|
|
292
|
+
PS: 29,
|
|
293
|
+
PT: 25,
|
|
294
|
+
QA: 29,
|
|
295
|
+
RO: 24,
|
|
296
|
+
RS: 22,
|
|
297
|
+
SA: 24,
|
|
298
|
+
SE: 24,
|
|
299
|
+
SI: 19,
|
|
300
|
+
SK: 24,
|
|
301
|
+
SM: 27,
|
|
302
|
+
TN: 24,
|
|
303
|
+
TR: 26,
|
|
304
|
+
UA: 29,
|
|
305
|
+
VG: 24,
|
|
306
|
+
XK: 20
|
|
307
|
+
};
|
|
308
|
+
function ibanValid(raw) {
|
|
309
|
+
const s = raw.replace(/\s/g, "").toUpperCase();
|
|
310
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
|
|
311
|
+
const expected = IBAN_LENGTHS[s.slice(0, 2)];
|
|
312
|
+
if (expected !== void 0 && s.length !== expected) return false;
|
|
313
|
+
if (expected === void 0) return false;
|
|
314
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
315
|
+
let rem = 0;
|
|
316
|
+
for (const ch of rearranged) {
|
|
317
|
+
const v = ch >= "A" ? String(ch.charCodeAt(0) - 55) : ch;
|
|
318
|
+
for (const digit of v) rem = (rem * 10 + (digit.charCodeAt(0) - 48)) % 97;
|
|
319
|
+
}
|
|
320
|
+
return rem === 1;
|
|
321
|
+
}
|
|
322
|
+
__name(ibanValid, "ibanValid");
|
|
323
|
+
function phoneValid(raw) {
|
|
324
|
+
const digits = raw.replace(/\D/g, "");
|
|
325
|
+
if (raw.trim().startsWith("+")) return digits.length >= 8 && digits.length <= 15;
|
|
326
|
+
if (digits.length < 10 || digits.length > 11) return false;
|
|
327
|
+
if (/^\(?\d{2}\)?[\s.-]?9?\d{4}[\s.-]\d{4}$/.test(raw.trim())) return true;
|
|
328
|
+
if (/^\(?\d{3}\)?[\s.-]?\d{3}[\s.-]\d{4}$/.test(raw.trim())) return true;
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
__name(phoneValid, "phoneValid");
|
|
332
|
+
var PATTERNS = [
|
|
333
|
+
{
|
|
334
|
+
type: "email",
|
|
335
|
+
re: /(?<![\w.+-])[A-Za-z0-9](?:[A-Za-z0-9._%+-]{0,63})@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,62})\.)+[A-Za-z]{2,24}(?![\w-])/g,
|
|
336
|
+
score: 0.99
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "iban",
|
|
340
|
+
re: /(?<![A-Za-z0-9])[A-Z]{2}\d{2}(?: ?[A-Z0-9]){11,30}(?![A-Za-z0-9])/g,
|
|
341
|
+
valid: ibanValid,
|
|
342
|
+
score: 0.99
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
type: "cnpj",
|
|
346
|
+
re: /(?<![\w./-])[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?\d{2}(?![\w/-])/g,
|
|
347
|
+
valid: /* @__PURE__ */ __name((m) => cnpjValid(m) && (/^\d/.test(m.replace(/\D/g, "")) || /[./-]/.test(m)) && !/^[A-Z]+$/.test(m), "valid"),
|
|
348
|
+
score: 0.98
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: "cpf",
|
|
352
|
+
re: /(?<![\w./-])\d{3}\.?\d{3}\.?\d{3}-?\d{2}(?![\w/-])/g,
|
|
353
|
+
valid: cpfValid,
|
|
354
|
+
score: 0.95
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
type: "credit_card",
|
|
358
|
+
re: /(?<![\d-])\d(?:[ -]?\d){11,18}(?![\d-])/g,
|
|
359
|
+
valid: /* @__PURE__ */ __name((m) => {
|
|
360
|
+
const digits = m.replace(/\D/g, "");
|
|
361
|
+
if (/ /.test(m) && /-/.test(m)) return false;
|
|
362
|
+
return digits.length >= 13 && digits.length <= 19 && luhnValid(digits) && !!cardBrand(digits);
|
|
363
|
+
}, "valid"),
|
|
364
|
+
score: 0.97
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
type: "ssn",
|
|
368
|
+
re: /(?<![\d-])\d{3}[- ]\d{2}[- ]\d{4}(?![\d-])/g,
|
|
369
|
+
valid: ssnValid,
|
|
370
|
+
score: 0.85
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
type: "phone",
|
|
374
|
+
re: /(?<![\w+])(?:\+\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,5}[\s.-]\d{4}(?![\w])/g,
|
|
375
|
+
valid: phoneValid,
|
|
376
|
+
score: 0.75
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
type: "ip_address",
|
|
380
|
+
re: /(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])/g,
|
|
381
|
+
valid: /* @__PURE__ */ __name((m) => isIPv4(m) && !m.startsWith("0.") && m !== "127.0.0.1", "valid"),
|
|
382
|
+
score: 0.6
|
|
383
|
+
}
|
|
384
|
+
];
|
|
385
|
+
function detectPii(text, types = PII_TYPES) {
|
|
386
|
+
const want = new Set(types);
|
|
387
|
+
const out = [];
|
|
388
|
+
for (const p of PATTERNS) {
|
|
389
|
+
if (!want.has(p.type)) continue;
|
|
390
|
+
p.re.lastIndex = 0;
|
|
391
|
+
for (let m = p.re.exec(text); m; m = p.re.exec(text)) {
|
|
392
|
+
const value = m[0];
|
|
393
|
+
if (p.valid && !p.valid(value)) continue;
|
|
394
|
+
out.push({
|
|
395
|
+
detector: "pii",
|
|
396
|
+
category: `pii.${p.type}`,
|
|
397
|
+
start: m.index,
|
|
398
|
+
end: m.index + value.length,
|
|
399
|
+
score: p.score,
|
|
400
|
+
value,
|
|
401
|
+
spanned: true
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
__name(detectPii, "detectPii");
|
|
408
|
+
|
|
409
|
+
// src/guardrails/detectors/secrets.ts
|
|
410
|
+
function entropy(s) {
|
|
411
|
+
if (!s) return 0;
|
|
412
|
+
const counts = /* @__PURE__ */ new Map();
|
|
413
|
+
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
414
|
+
let h = 0;
|
|
415
|
+
for (const c of counts.values()) {
|
|
416
|
+
const p = c / s.length;
|
|
417
|
+
h -= p * Math.log2(p);
|
|
418
|
+
}
|
|
419
|
+
return h;
|
|
420
|
+
}
|
|
421
|
+
__name(entropy, "entropy");
|
|
422
|
+
var PATTERNS2 = [
|
|
423
|
+
{
|
|
424
|
+
type: "private_key",
|
|
425
|
+
re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|$)/g,
|
|
426
|
+
score: 0.99
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
type: "anthropic_key",
|
|
430
|
+
re: /(?<![\w-])sk-ant-(?:api|admin|oat)\d{0,2}-?[A-Za-z0-9_-]{20,}/g,
|
|
431
|
+
score: 0.99
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: "openai_key",
|
|
435
|
+
re: /(?<![\w-])sk-(?!ant-)(?:proj-|svcacct-|admin-|None-)?[A-Za-z0-9_-]{20,}/g,
|
|
436
|
+
score: 0.95,
|
|
437
|
+
valid: /* @__PURE__ */ __name((v) => entropy(v) > 3.5, "valid")
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
type: "aws_access_key",
|
|
441
|
+
re: /(?<![A-Z0-9])(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}(?![A-Z0-9])/g,
|
|
442
|
+
score: 0.98
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: "aws_secret_key",
|
|
446
|
+
re: /(?:aws_?secret_?access_?key|aws_?secret|secretaccesskey)["']?\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,
|
|
447
|
+
group: 1,
|
|
448
|
+
score: 0.97
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
type: "github_token",
|
|
452
|
+
re: /(?<![\w])(?:(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255})/g,
|
|
453
|
+
score: 0.99
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: "gitlab_token",
|
|
457
|
+
re: /(?<![\w-])glpat-[A-Za-z0-9_-]{20,}/g,
|
|
458
|
+
score: 0.99
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: "slack_token",
|
|
462
|
+
re: /(?<![\w-])(?:xox[abprseo]-[A-Za-z0-9-]{10,}|https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]{20,})/g,
|
|
463
|
+
score: 0.98
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: "google_api_key",
|
|
467
|
+
re: /(?<![\w-])AIza[0-9A-Za-z_-]{35}(?![\w-])/g,
|
|
468
|
+
score: 0.97
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
type: "stripe_key",
|
|
472
|
+
re: /(?<![\w])(?:sk|rk|pk)_(?:live|test)_[0-9A-Za-z]{16,}/g,
|
|
473
|
+
score: 0.97
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
type: "jwt",
|
|
477
|
+
re: /(?<![\w-])eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?![\w-])/g,
|
|
478
|
+
score: 0.9
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
type: "bearer_token",
|
|
482
|
+
re: /\bbearer\s+([A-Za-z0-9._~+/-]{20,}=*)/gi,
|
|
483
|
+
group: 1,
|
|
484
|
+
score: 0.85,
|
|
485
|
+
valid: /* @__PURE__ */ __name((v) => entropy(v) > 3.5, "valid")
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
type: "generic_secret",
|
|
489
|
+
re: /\b(?:api[_-]?key|api[_-]?secret|client[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|password|passwd|senha)\b["']?\s*[:=]\s*["']?([^\s"',;]{8,200})/gi,
|
|
490
|
+
group: 1,
|
|
491
|
+
score: 0.7,
|
|
492
|
+
// Words and placeholders ("changeme", "<your-key>", "${API_KEY}") are not secrets.
|
|
493
|
+
valid: /* @__PURE__ */ __name((v) => entropy(v) >= 3 && !/^[<{$[]/.test(v) && !/^(?:x+|\*+|\.+)$/i.test(v), "valid")
|
|
494
|
+
}
|
|
495
|
+
];
|
|
496
|
+
function detectSecrets(text, types = SECRET_TYPES) {
|
|
497
|
+
const want = new Set(types);
|
|
498
|
+
const out = [];
|
|
499
|
+
for (const p of PATTERNS2) {
|
|
500
|
+
if (!want.has(p.type)) continue;
|
|
501
|
+
p.re.lastIndex = 0;
|
|
502
|
+
for (let m = p.re.exec(text); m; m = p.re.exec(text)) {
|
|
503
|
+
if (m[0].length === 0) {
|
|
504
|
+
p.re.lastIndex++;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const group = p.group ?? 0;
|
|
508
|
+
const value = m[group];
|
|
509
|
+
if (!value) continue;
|
|
510
|
+
if (p.valid && !p.valid(value)) continue;
|
|
511
|
+
const start = m.index + (group === 0 ? 0 : m[0].lastIndexOf(value));
|
|
512
|
+
out.push({
|
|
513
|
+
detector: "secrets",
|
|
514
|
+
category: `secret.${p.type}`,
|
|
515
|
+
start,
|
|
516
|
+
end: start + value.length,
|
|
517
|
+
score: p.score,
|
|
518
|
+
value,
|
|
519
|
+
spanned: true
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
__name(detectSecrets, "detectSecrets");
|
|
526
|
+
|
|
527
|
+
// src/guardrails/detectors/injection.ts
|
|
528
|
+
var W = String.raw`[\s\S]{0,40}?`;
|
|
529
|
+
var INJECTION_SIGNALS = [
|
|
530
|
+
// Instruction override.
|
|
531
|
+
{
|
|
532
|
+
id: "ignore_previous",
|
|
533
|
+
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"),
|
|
534
|
+
weight: 0.75
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
id: "ignore_previous_pt",
|
|
538
|
+
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"),
|
|
539
|
+
weight: 0.75
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "ignore_previous_es",
|
|
543
|
+
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"),
|
|
544
|
+
weight: 0.75
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: "new_instructions",
|
|
548
|
+
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,
|
|
549
|
+
weight: 0.45
|
|
550
|
+
},
|
|
551
|
+
// Role hijack / persona jailbreaks.
|
|
552
|
+
{
|
|
553
|
+
id: "role_hijack",
|
|
554
|
+
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,
|
|
555
|
+
weight: 0.45
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "jailbreak_persona",
|
|
559
|
+
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,
|
|
560
|
+
weight: 0.55
|
|
561
|
+
},
|
|
562
|
+
// Prompt / secret extraction.
|
|
563
|
+
{
|
|
564
|
+
id: "prompt_leak",
|
|
565
|
+
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,
|
|
566
|
+
weight: 0.6
|
|
567
|
+
},
|
|
568
|
+
// Role / delimiter spoofing: chat-template tokens and fake headers.
|
|
569
|
+
{
|
|
570
|
+
id: "role_tokens",
|
|
571
|
+
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,
|
|
572
|
+
weight: 0.55
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "fake_system",
|
|
576
|
+
re: /^\s*(?:system|assistant|developer)\s*(?:message|prompt|note|override)?\s*:\s*\S/gim,
|
|
577
|
+
weight: 0.3
|
|
578
|
+
},
|
|
579
|
+
// Indirect injection aimed at the agent reading the content.
|
|
580
|
+
{
|
|
581
|
+
id: "addressed_to_ai",
|
|
582
|
+
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,
|
|
583
|
+
weight: 0.55
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: "tool_directive",
|
|
587
|
+
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,
|
|
588
|
+
weight: 0.45
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: "secrecy",
|
|
592
|
+
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,
|
|
593
|
+
weight: 0.5
|
|
594
|
+
},
|
|
595
|
+
// Exfiltration through rendered markdown/HTML.
|
|
596
|
+
{
|
|
597
|
+
id: "exfil_markdown",
|
|
598
|
+
re: /!\[[^\]]*\]\(\s*https?:\/\/[^)\s]+\?[^)\s]*=[^)\s]*(?:\{|%7B|\$|<)?[^)]*\)|<img\b[^>]*\bsrc\s*=\s*["']?https?:\/\/[^"'>\s]+\?[^"'>\s]*=/gi,
|
|
599
|
+
weight: 0.4
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: "html_comment_instructions",
|
|
603
|
+
re: /<!--[\s\S]{0,500}?\b(?:ignore|instruction|assistant|ai|agent|system|prompt|instru[çc][ãa]o|assistente)\b[\s\S]{0,500}?-->/gi,
|
|
604
|
+
weight: 0.45
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
var TAG_CHARS = /[\u{E0000}-\u{E007F}]+/gu;
|
|
608
|
+
var BIDI = /[\u202A-\u202E\u2066-\u2069]/g;
|
|
609
|
+
var ZERO_WIDTH_RUN = /[\u200B-\u200D\u2060\uFEFF]{3,}/g;
|
|
610
|
+
function decodeBase64Ascii(b64) {
|
|
611
|
+
try {
|
|
612
|
+
return atob(b64);
|
|
613
|
+
} catch {
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
__name(decodeBase64Ascii, "decodeBase64Ascii");
|
|
618
|
+
function decodeTagChars(s) {
|
|
619
|
+
let out = "";
|
|
620
|
+
for (const ch of s) {
|
|
621
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
622
|
+
if (cp >= 917536 && cp <= 917630) out += String.fromCharCode(cp - 917504);
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
__name(decodeTagChars, "decodeTagChars");
|
|
627
|
+
function matchSignals(text, offset, into, weights) {
|
|
628
|
+
for (const s of INJECTION_SIGNALS) {
|
|
629
|
+
s.re.lastIndex = 0;
|
|
630
|
+
for (let m = s.re.exec(text); m; m = s.re.exec(text)) {
|
|
631
|
+
if (m[0].length === 0) {
|
|
632
|
+
s.re.lastIndex++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
weights.set(s.id, Math.max(weights.get(s.id) ?? 0, s.weight));
|
|
636
|
+
const list = into.get(s.id) ?? [];
|
|
637
|
+
list.push({
|
|
638
|
+
detector: "injection",
|
|
639
|
+
category: `injection.${s.id}`,
|
|
640
|
+
start: offset < 0 ? m.index : offset,
|
|
641
|
+
end: offset < 0 ? m.index + m[0].length : offset,
|
|
642
|
+
score: s.weight,
|
|
643
|
+
value: m[0],
|
|
644
|
+
spanned: offset < 0
|
|
645
|
+
});
|
|
646
|
+
into.set(s.id, list);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
__name(matchSignals, "matchSignals");
|
|
651
|
+
function scoreInjection(text, threshold = 0.5) {
|
|
652
|
+
const byId = /* @__PURE__ */ new Map();
|
|
653
|
+
const weights = /* @__PURE__ */ new Map();
|
|
654
|
+
matchSignals(text, -1, byId, weights);
|
|
655
|
+
TAG_CHARS.lastIndex = 0;
|
|
656
|
+
for (let m = TAG_CHARS.exec(text); m; m = TAG_CHARS.exec(text)) {
|
|
657
|
+
weights.set("hidden_unicode", 0.8);
|
|
658
|
+
const list = byId.get("hidden_unicode") ?? [];
|
|
659
|
+
list.push({
|
|
660
|
+
detector: "injection",
|
|
661
|
+
category: "injection.hidden_unicode",
|
|
662
|
+
start: m.index,
|
|
663
|
+
end: m.index + m[0].length,
|
|
664
|
+
score: 0.8,
|
|
665
|
+
value: m[0],
|
|
666
|
+
spanned: true
|
|
667
|
+
});
|
|
668
|
+
byId.set("hidden_unicode", list);
|
|
669
|
+
const decoded = decodeTagChars(m[0]);
|
|
670
|
+
if (decoded) matchSignals(decoded, m.index, byId, weights);
|
|
671
|
+
}
|
|
672
|
+
for (const re of [
|
|
673
|
+
BIDI,
|
|
674
|
+
ZERO_WIDTH_RUN
|
|
675
|
+
]) {
|
|
676
|
+
re.lastIndex = 0;
|
|
677
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
678
|
+
weights.set("invisible_chars", Math.max(weights.get("invisible_chars") ?? 0, 0.3));
|
|
679
|
+
const list = byId.get("invisible_chars") ?? [];
|
|
680
|
+
list.push({
|
|
681
|
+
detector: "injection",
|
|
682
|
+
category: "injection.invisible_chars",
|
|
683
|
+
start: m.index,
|
|
684
|
+
end: m.index + m[0].length,
|
|
685
|
+
score: 0.3,
|
|
686
|
+
value: m[0],
|
|
687
|
+
spanned: true
|
|
688
|
+
});
|
|
689
|
+
byId.set("invisible_chars", list);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const B64 = /(?<![A-Za-z0-9+/=])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g;
|
|
693
|
+
for (let m = B64.exec(text); m; m = B64.exec(text)) {
|
|
694
|
+
const decoded = decodeBase64Ascii(m[0]);
|
|
695
|
+
if (decoded === void 0) continue;
|
|
696
|
+
if (!/^[\x20-\x7E\s]+$/.test(decoded)) continue;
|
|
697
|
+
const inner = /* @__PURE__ */ new Map();
|
|
698
|
+
const innerWeights = /* @__PURE__ */ new Map();
|
|
699
|
+
matchSignals(decoded, m.index, inner, innerWeights);
|
|
700
|
+
if (innerWeights.size > 0) {
|
|
701
|
+
weights.set("encoded_instructions", 0.6);
|
|
702
|
+
byId.set("encoded_instructions", [
|
|
703
|
+
{
|
|
704
|
+
detector: "injection",
|
|
705
|
+
category: "injection.encoded_instructions",
|
|
706
|
+
start: m.index,
|
|
707
|
+
end: m.index + m[0].length,
|
|
708
|
+
score: 0.6,
|
|
709
|
+
value: m[0],
|
|
710
|
+
spanned: true
|
|
711
|
+
}
|
|
712
|
+
]);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
let keep = 1;
|
|
716
|
+
for (const w of weights.values()) keep *= 1 - w;
|
|
717
|
+
const score = Math.round((1 - keep) * 1e3) / 1e3;
|
|
718
|
+
if (score < threshold) return {
|
|
719
|
+
score,
|
|
720
|
+
findings: []
|
|
721
|
+
};
|
|
722
|
+
const findings = [
|
|
723
|
+
...byId.values()
|
|
724
|
+
].flat().map((f) => ({
|
|
725
|
+
...f,
|
|
726
|
+
score: Math.max(f.score, score)
|
|
727
|
+
}));
|
|
728
|
+
return {
|
|
729
|
+
score,
|
|
730
|
+
findings
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
__name(scoreInjection, "scoreInjection");
|
|
734
|
+
|
|
735
|
+
// src/guardrails/detectors/tool-poisoning.ts
|
|
736
|
+
var POISON_SIGNALS = [
|
|
737
|
+
{
|
|
738
|
+
id: "important_tag",
|
|
739
|
+
re: /<\s*\/?\s*(?:important|system|instructions?|secret|hidden|critical)\s*>/gi,
|
|
740
|
+
weight: 0.55
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
id: "sensitive_files",
|
|
744
|
+
re: /(?:~\/\.ssh|id_rsa|id_ed25519|\.aws\/credentials|\.env\b|mcp\.json|\.netrc|\.npmrc|\.pypirc|\.git-credentials|\/etc\/passwd|\/etc\/shadow|kubeconfig|\.kube\/config|credentials\.json|cookies\.sqlite|wallet\.dat)/gi,
|
|
745
|
+
weight: 0.65
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
id: "read_before_use",
|
|
749
|
+
re: /\b(?:before|prior\s+to)\s+(?:using|calling|invoking|running)\s+(?:this|any|the)\s+(?:tool|function)\b[\s\S]{0,80}?\b(?:read|open|cat|fetch|call|send|include|pass|load)\b/gi,
|
|
750
|
+
weight: 0.5
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
id: "hidden_parameter",
|
|
754
|
+
re: /\b(?:pass|put|include|add|send|store)\b[\s\S]{0,40}?\b(?:content|contents|value|output|result|key|token|file)s?\b[\s\S]{0,40}?\b(?:as|in|into)\s+(?:the\s+)?[`'"]?(?:sidenote|side_note|note|notes|metadata|context|extra|comment|debug)[`'"]?\b/gi,
|
|
755
|
+
weight: 0.55
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: "cross_tool",
|
|
759
|
+
re: /\b(?:when|whenever|if)\s+(?:the\s+)?[`'"]?[\w.-]+[`'"]?\s+(?:tool|function)\s+is\s+(?:called|used|invoked)\b|\b(?:instead\s+of|rather\s+than)\s+(?:calling|using)\s+(?:the\s+)?[`'"]?[\w.-]+[`'"]?\s*(?:tool|function)?\b|\b(?:all|every)\s+(?:emails?|messages?)\s+(?:must|should)\s+(?:be\s+)?(?:sent|forwarded|bcc'?d|cc'?d)\s+to\b/gi,
|
|
760
|
+
weight: 0.5
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
id: "override_behaviour",
|
|
764
|
+
re: /\b(?:this\s+(?:tool|instruction)\s+(?:takes|has)\s+(?:precedence|priority)|overrides?\s+(?:all\s+)?(?:other|previous)\s+(?:tools?|instructions?)|very\s+(?:very\s+)?important\s*:?|you\s+must\s+(?:always|first|never))\b/gi,
|
|
765
|
+
weight: 0.35
|
|
766
|
+
}
|
|
767
|
+
];
|
|
768
|
+
var LONG_DESCRIPTION = 2e3;
|
|
769
|
+
function scoreToolText(text, threshold = 0.5) {
|
|
770
|
+
const weights = /* @__PURE__ */ new Map();
|
|
771
|
+
const findings = [];
|
|
772
|
+
for (const s of POISON_SIGNALS) {
|
|
773
|
+
s.re.lastIndex = 0;
|
|
774
|
+
for (let m = s.re.exec(text); m; m = s.re.exec(text)) {
|
|
775
|
+
weights.set(s.id, s.weight);
|
|
776
|
+
findings.push({
|
|
777
|
+
detector: "tool_poisoning",
|
|
778
|
+
category: `tool_poisoning.${s.id}`,
|
|
779
|
+
start: m.index,
|
|
780
|
+
end: m.index + m[0].length,
|
|
781
|
+
score: s.weight,
|
|
782
|
+
value: m[0],
|
|
783
|
+
spanned: true
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const injection = scoreInjection(text, 0);
|
|
788
|
+
for (const f of injection.findings) {
|
|
789
|
+
const id = f.category.slice("injection.".length);
|
|
790
|
+
const sig = INJECTION_SIGNALS.find((s) => s.id === id);
|
|
791
|
+
weights.set(`inj:${id}`, sig?.weight ?? f.score);
|
|
792
|
+
findings.push({
|
|
793
|
+
...f,
|
|
794
|
+
detector: "tool_poisoning",
|
|
795
|
+
category: `tool_poisoning.${id}`
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
if (text.length > LONG_DESCRIPTION) weights.set("long_description", 0.2);
|
|
799
|
+
let keep = 1;
|
|
800
|
+
for (const w of weights.values()) keep *= 1 - w;
|
|
801
|
+
const score = Math.round((1 - keep) * 1e3) / 1e3;
|
|
802
|
+
return score >= threshold ? {
|
|
803
|
+
score,
|
|
804
|
+
findings
|
|
805
|
+
} : {
|
|
806
|
+
score,
|
|
807
|
+
findings: []
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
__name(scoreToolText, "scoreToolText");
|
|
811
|
+
function toolText(tool) {
|
|
812
|
+
const parts = [];
|
|
813
|
+
if (tool.title) parts.push(tool.title);
|
|
814
|
+
if (tool.description) parts.push(tool.description);
|
|
815
|
+
const walk = /* @__PURE__ */ __name((schema, depth) => {
|
|
816
|
+
if (!schema || typeof schema !== "object" || depth > 6) return;
|
|
817
|
+
const s = schema;
|
|
818
|
+
if (typeof s.description === "string") parts.push(s.description);
|
|
819
|
+
if (typeof s.title === "string") parts.push(s.title);
|
|
820
|
+
for (const key of [
|
|
821
|
+
"properties",
|
|
822
|
+
"definitions",
|
|
823
|
+
"$defs"
|
|
824
|
+
]) {
|
|
825
|
+
const props = s[key];
|
|
826
|
+
if (props && typeof props === "object") for (const [name, child] of Object.entries(props)) {
|
|
827
|
+
parts.push(name);
|
|
828
|
+
walk(child, depth + 1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
for (const key of [
|
|
832
|
+
"items",
|
|
833
|
+
"additionalProperties"
|
|
834
|
+
]) walk(s[key], depth + 1);
|
|
835
|
+
for (const key of [
|
|
836
|
+
"anyOf",
|
|
837
|
+
"oneOf",
|
|
838
|
+
"allOf"
|
|
839
|
+
]) {
|
|
840
|
+
const list = s[key];
|
|
841
|
+
if (Array.isArray(list)) for (const child of list) walk(child, depth + 1);
|
|
842
|
+
}
|
|
843
|
+
if (Array.isArray(s.enum)) {
|
|
844
|
+
for (const e of s.enum) if (typeof e === "string") parts.push(e);
|
|
845
|
+
}
|
|
846
|
+
if (typeof s.default === "string") parts.push(s.default);
|
|
847
|
+
}, "walk");
|
|
848
|
+
walk(tool.inputSchema, 0);
|
|
849
|
+
return parts.join("\n");
|
|
850
|
+
}
|
|
851
|
+
__name(toolText, "toolText");
|
|
852
|
+
|
|
853
|
+
// src/guardrails/detectors/custom.ts
|
|
854
|
+
function compileFlagged(pattern) {
|
|
855
|
+
return pattern.startsWith("(?i)") ? new RegExp(pattern.slice(4), "i") : new RegExp(pattern);
|
|
856
|
+
}
|
|
857
|
+
__name(compileFlagged, "compileFlagged");
|
|
858
|
+
var compiled = /* @__PURE__ */ new Map();
|
|
859
|
+
function compileGuardPattern(pattern) {
|
|
860
|
+
let re = compiled.get(pattern);
|
|
861
|
+
if (!re) {
|
|
862
|
+
const base = compileFlagged(pattern);
|
|
863
|
+
re = new RegExp(base.source, `${base.flags.replace("g", "")}g`);
|
|
864
|
+
if (compiled.size > 5e3) compiled.clear();
|
|
865
|
+
compiled.set(pattern, re);
|
|
866
|
+
}
|
|
867
|
+
return re;
|
|
868
|
+
}
|
|
869
|
+
__name(compileGuardPattern, "compileGuardPattern");
|
|
870
|
+
function detectRegex(text, patterns, label = "custom") {
|
|
871
|
+
const out = [];
|
|
872
|
+
for (const p of patterns) {
|
|
873
|
+
let re;
|
|
874
|
+
try {
|
|
875
|
+
re = compileGuardPattern(p);
|
|
876
|
+
} catch {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
re.lastIndex = 0;
|
|
880
|
+
let guard = 0;
|
|
881
|
+
for (let m = re.exec(text); m && guard < 1e3; m = re.exec(text), guard++) {
|
|
882
|
+
if (m[0].length === 0) {
|
|
883
|
+
re.lastIndex++;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
out.push({
|
|
887
|
+
detector: "regex",
|
|
888
|
+
category: `regex.${label}`,
|
|
889
|
+
start: m.index,
|
|
890
|
+
end: m.index + m[0].length,
|
|
891
|
+
score: 1,
|
|
892
|
+
value: m[0],
|
|
893
|
+
spanned: true
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
899
|
+
__name(detectRegex, "detectRegex");
|
|
900
|
+
var COMBINING = /[\u0300-\u036f]/g;
|
|
901
|
+
function fold(s) {
|
|
902
|
+
let out = "";
|
|
903
|
+
for (const ch of s) {
|
|
904
|
+
const base = ch.normalize("NFD").replace(COMBINING, "");
|
|
905
|
+
const c = (base.length === ch.length ? base : ch).toLowerCase();
|
|
906
|
+
out += c.length === ch.length ? c : ch;
|
|
907
|
+
}
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
__name(fold, "fold");
|
|
911
|
+
var escapeRe = /* @__PURE__ */ __name((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "escapeRe");
|
|
912
|
+
function detectKeywords(text, words, label = "keywords") {
|
|
913
|
+
const clean = words.map((w) => fold(w.trim())).filter(Boolean);
|
|
914
|
+
if (clean.length === 0) return [];
|
|
915
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_])(?:${clean.map(escapeRe).join("|")})(?![\\p{L}\\p{N}_])`, "gu");
|
|
916
|
+
const folded = fold(text);
|
|
917
|
+
const out = [];
|
|
918
|
+
for (let m = re.exec(folded); m; m = re.exec(folded)) {
|
|
919
|
+
out.push({
|
|
920
|
+
detector: "keywords",
|
|
921
|
+
category: `keywords.${label}`,
|
|
922
|
+
start: m.index,
|
|
923
|
+
end: m.index + m[0].length,
|
|
924
|
+
score: 1,
|
|
925
|
+
value: text.slice(m.index, m.index + m[0].length),
|
|
926
|
+
spanned: true
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
931
|
+
__name(detectKeywords, "detectKeywords");
|
|
932
|
+
|
|
933
|
+
// src/guardrails/glob.ts
|
|
934
|
+
var cache = /* @__PURE__ */ new Map();
|
|
935
|
+
function globToRegExp(pattern) {
|
|
936
|
+
let re = cache.get(pattern);
|
|
937
|
+
if (!re) {
|
|
938
|
+
const body = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
939
|
+
re = new RegExp(`^${body}$`);
|
|
940
|
+
cache.set(pattern, re);
|
|
941
|
+
}
|
|
942
|
+
return re;
|
|
943
|
+
}
|
|
944
|
+
__name(globToRegExp, "globToRegExp");
|
|
945
|
+
function matchesAny(value, patterns) {
|
|
946
|
+
if (!patterns || patterns.length === 0) return true;
|
|
947
|
+
return patterns.some((p) => globToRegExp(p).test(value));
|
|
948
|
+
}
|
|
949
|
+
__name(matchesAny, "matchesAny");
|
|
950
|
+
|
|
951
|
+
// src/guardrails/vault.ts
|
|
952
|
+
var Vault = class _Vault {
|
|
953
|
+
static {
|
|
954
|
+
__name(this, "Vault");
|
|
955
|
+
}
|
|
956
|
+
byValue = /* @__PURE__ */ new Map();
|
|
957
|
+
byToken = /* @__PURE__ */ new Map();
|
|
958
|
+
counters = /* @__PURE__ */ new Map();
|
|
959
|
+
/**
|
|
960
|
+
* Placeholder for `value` under `label` (e.g. `CREDIT_CARD`). Reversible placeholders look like
|
|
961
|
+
* `[CREDIT_CARD_1]`; one-way ones (tool results, answers, rules with `restore: false`) like
|
|
962
|
+
* `[REDACTED_CREDIT_CARD_1]`, so they can never be mistaken for a reversible one.
|
|
963
|
+
*/
|
|
964
|
+
tokenFor(label, value, restorable) {
|
|
965
|
+
const ns = restorable ? label : `REDACTED_${label}`;
|
|
966
|
+
const key = `${ns}\0${value}`;
|
|
967
|
+
const existing = this.byValue.get(key);
|
|
968
|
+
if (existing) return existing;
|
|
969
|
+
const n = (this.counters.get(ns) ?? 0) + 1;
|
|
970
|
+
this.counters.set(ns, n);
|
|
971
|
+
const token = `[${ns}_${n}]`;
|
|
972
|
+
this.byValue.set(key, token);
|
|
973
|
+
this.byToken.set(token, {
|
|
974
|
+
value,
|
|
975
|
+
restorable
|
|
976
|
+
});
|
|
977
|
+
return token;
|
|
978
|
+
}
|
|
979
|
+
get size() {
|
|
980
|
+
return this.byToken.size;
|
|
981
|
+
}
|
|
982
|
+
get restorableCount() {
|
|
983
|
+
let n = 0;
|
|
984
|
+
for (const e of this.byToken.values()) if (e.restorable) n++;
|
|
985
|
+
return n;
|
|
986
|
+
}
|
|
987
|
+
/** Longest placeholder, for stream hold-back windows. */
|
|
988
|
+
get maxTokenLength() {
|
|
989
|
+
let n = 0;
|
|
990
|
+
for (const t of this.byToken.keys()) n = Math.max(n, t.length);
|
|
991
|
+
return n;
|
|
992
|
+
}
|
|
993
|
+
/** Puts restorable values back. `jsonString`: the text is inside a JSON string literal. */
|
|
994
|
+
restore(text, jsonString = false) {
|
|
995
|
+
if (this.byToken.size === 0 || !text.includes("[")) return text;
|
|
996
|
+
return text.replace(/\[([A-Z][A-Z0-9_]*_\d+)\]/g, (whole) => {
|
|
997
|
+
const entry = this.byToken.get(whole);
|
|
998
|
+
if (!entry?.restorable) return whole;
|
|
999
|
+
return jsonString ? JSON.stringify(entry.value).slice(1, -1) : entry.value;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/** Whether `text` ends with what could be the beginning of a placeholder (streaming). */
|
|
1003
|
+
pendingPrefix(text) {
|
|
1004
|
+
const i = text.lastIndexOf("[");
|
|
1005
|
+
if (i === -1) return 0;
|
|
1006
|
+
const tail = text.slice(i);
|
|
1007
|
+
if (tail.length > this.maxTokenLength) return 0;
|
|
1008
|
+
return /^\[[A-Z0-9_]*$/.test(tail) ? tail.length : 0;
|
|
1009
|
+
}
|
|
1010
|
+
/** Every entry, in the order the placeholders were minted. */
|
|
1011
|
+
toJSON() {
|
|
1012
|
+
return {
|
|
1013
|
+
entries: [
|
|
1014
|
+
...this.byToken
|
|
1015
|
+
].map(([token, e]) => ({
|
|
1016
|
+
token,
|
|
1017
|
+
value: e.value,
|
|
1018
|
+
restorable: e.restorable
|
|
1019
|
+
}))
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
/** Rebuilds a vault from {@link toJSON}; placeholders keep their numbers and counters resume. */
|
|
1023
|
+
static fromJSON(snapshot) {
|
|
1024
|
+
const vault = new _Vault();
|
|
1025
|
+
for (const { token, value, restorable } of snapshot.entries) {
|
|
1026
|
+
const m = /^\[([A-Z][A-Z0-9_]*)_(\d+)\]$/.exec(token);
|
|
1027
|
+
if (!m) continue;
|
|
1028
|
+
const ns = m[1] ?? "";
|
|
1029
|
+
const n = Number(m[2]);
|
|
1030
|
+
vault.byValue.set(`${ns}\0${value}`, token);
|
|
1031
|
+
vault.byToken.set(token, {
|
|
1032
|
+
value,
|
|
1033
|
+
restorable
|
|
1034
|
+
});
|
|
1035
|
+
vault.counters.set(ns, Math.max(vault.counters.get(ns) ?? 0, n));
|
|
1036
|
+
}
|
|
1037
|
+
return vault;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
function labelFor(category) {
|
|
1041
|
+
const tail = category.includes(".") ? category.slice(category.indexOf(".") + 1) : category;
|
|
1042
|
+
const label = tail.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
1043
|
+
return label || "REDACTED";
|
|
1044
|
+
}
|
|
1045
|
+
__name(labelFor, "labelFor");
|
|
1046
|
+
|
|
1047
|
+
// src/guardrails/engine.ts
|
|
1048
|
+
var DEFAULT_MESSAGES = {
|
|
1049
|
+
llm_request: "This message was blocked by your organization's AI guardrails.",
|
|
1050
|
+
llm_response: "The answer was withheld by your organization's AI guardrails.",
|
|
1051
|
+
tool_args: "This tool call was blocked by your organization's AI guardrails.",
|
|
1052
|
+
tool_result: "The tool's result was withheld by your organization's AI guardrails.",
|
|
1053
|
+
tool_description: "This tool was quarantined by your organization's AI guardrails."
|
|
1054
|
+
};
|
|
1055
|
+
var DEFAULT_APPROVAL_MESSAGE = "This needs approval under your organization's AI guardrails before it can go ahead.";
|
|
1056
|
+
var INJECTION_REPLACEMENT = "[removed: suspected prompt injection]";
|
|
1057
|
+
var ruleName = /* @__PURE__ */ __name((rule) => rule.name ?? rule.id, "ruleName");
|
|
1058
|
+
function ruleApplies(rule, ctx) {
|
|
1059
|
+
if (rule.enabled === false || !rule.stages.includes(ctx.stage)) return false;
|
|
1060
|
+
const tools = rule.match?.tools;
|
|
1061
|
+
if (tools?.length) {
|
|
1062
|
+
const toolStage = ctx.stage === "tool_args" || ctx.stage === "tool_result" || ctx.stage === "tool_description";
|
|
1063
|
+
if (!toolStage || !matchesAny(ctx.tool ?? "", tools)) return false;
|
|
1064
|
+
}
|
|
1065
|
+
return rule.when === void 0 || rule.when(ctx);
|
|
1066
|
+
}
|
|
1067
|
+
__name(ruleApplies, "ruleApplies");
|
|
1068
|
+
function orderRules(rules) {
|
|
1069
|
+
return [
|
|
1070
|
+
...rules
|
|
1071
|
+
].sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100) || ruleName(a).localeCompare(ruleName(b)));
|
|
1072
|
+
}
|
|
1073
|
+
__name(orderRules, "orderRules");
|
|
1074
|
+
async function runDetector(spec, text, ctx) {
|
|
1075
|
+
if (!text) return [];
|
|
1076
|
+
switch (spec.kind) {
|
|
1077
|
+
case "pii":
|
|
1078
|
+
return detectPii(text, spec.types?.length ? spec.types : void 0);
|
|
1079
|
+
case "secrets":
|
|
1080
|
+
return detectSecrets(text, spec.types?.length ? spec.types : void 0);
|
|
1081
|
+
case "injection":
|
|
1082
|
+
return scoreInjection(text, spec.threshold ?? 0.5).findings;
|
|
1083
|
+
case "tool_poisoning":
|
|
1084
|
+
return scoreToolText(text, spec.threshold ?? 0.5).findings;
|
|
1085
|
+
case "regex":
|
|
1086
|
+
return detectRegex(text, spec.patterns, spec.label ?? "custom");
|
|
1087
|
+
case "keywords":
|
|
1088
|
+
return detectKeywords(text, spec.words, spec.label ?? "keywords");
|
|
1089
|
+
case "custom":
|
|
1090
|
+
return spec.detect(text, ctx);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
__name(runDetector, "runDetector");
|
|
1094
|
+
function spanLocator(rules) {
|
|
1095
|
+
const specs = rules.flatMap((r) => r.detectors).filter((d) => d.kind !== "custom" && d.kind !== "tool_poisoning");
|
|
1096
|
+
if (specs.length === 0) return void 0;
|
|
1097
|
+
return (text) => specs.flatMap((d) => {
|
|
1098
|
+
switch (d.kind) {
|
|
1099
|
+
case "pii":
|
|
1100
|
+
return detectPii(text, d.types?.length ? d.types : void 0);
|
|
1101
|
+
case "secrets":
|
|
1102
|
+
return detectSecrets(text, d.types?.length ? d.types : void 0);
|
|
1103
|
+
case "regex":
|
|
1104
|
+
return detectRegex(text, d.patterns);
|
|
1105
|
+
case "keywords":
|
|
1106
|
+
return detectKeywords(text, d.words);
|
|
1107
|
+
case "injection":
|
|
1108
|
+
return scoreInjection(text, 0).findings;
|
|
1109
|
+
default:
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
__name(spanLocator, "spanLocator");
|
|
1115
|
+
function resolveOverlaps(findings, text) {
|
|
1116
|
+
const sorted = [
|
|
1117
|
+
...findings
|
|
1118
|
+
].filter((f) => f.spanned !== false && f.end > f.start).sort((a, b) => a.start - b.start || b.end - a.end || b.score - a.score);
|
|
1119
|
+
const out = [];
|
|
1120
|
+
for (const f of sorted) {
|
|
1121
|
+
const last = out[out.length - 1];
|
|
1122
|
+
if (!last || f.start >= last.end) {
|
|
1123
|
+
out.push({
|
|
1124
|
+
...f
|
|
1125
|
+
});
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const longer = f.end - f.start > last.end - last.start || f.end - f.start === last.end - last.start && f.score > last.score ? f : last;
|
|
1129
|
+
const merged = {
|
|
1130
|
+
...longer,
|
|
1131
|
+
start: last.start,
|
|
1132
|
+
end: Math.max(last.end, f.end)
|
|
1133
|
+
};
|
|
1134
|
+
if (text !== void 0) merged.value = text.slice(merged.start, merged.end);
|
|
1135
|
+
out[out.length - 1] = merged;
|
|
1136
|
+
}
|
|
1137
|
+
return out;
|
|
1138
|
+
}
|
|
1139
|
+
__name(resolveOverlaps, "resolveOverlaps");
|
|
1140
|
+
function replacementFor(f, vault, restorable) {
|
|
1141
|
+
if (f.replacement !== void 0) return f.replacement;
|
|
1142
|
+
if (f.detector === "injection" || f.detector === "tool_poisoning") return INJECTION_REPLACEMENT;
|
|
1143
|
+
return vault.tokenFor(labelFor(f.category), f.value, restorable);
|
|
1144
|
+
}
|
|
1145
|
+
__name(replacementFor, "replacementFor");
|
|
1146
|
+
function applyRedactions(text, findings, vault, restorable) {
|
|
1147
|
+
const whole = findings.find((f) => f.spanned === false);
|
|
1148
|
+
if (whole) {
|
|
1149
|
+
return {
|
|
1150
|
+
text: whole.replacement ?? `[removed: ${whole.category}]`,
|
|
1151
|
+
count: 1
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
const spans = resolveOverlaps(findings, text);
|
|
1155
|
+
if (spans.length === 0) return {
|
|
1156
|
+
text,
|
|
1157
|
+
count: 0
|
|
1158
|
+
};
|
|
1159
|
+
let out = "";
|
|
1160
|
+
let at = 0;
|
|
1161
|
+
for (const f of spans) {
|
|
1162
|
+
out += text.slice(at, f.start) + replacementFor(f, vault, restorable);
|
|
1163
|
+
at = f.end;
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
text: out + text.slice(at),
|
|
1167
|
+
count: spans.length
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
__name(applyRedactions, "applyRedactions");
|
|
1171
|
+
function memoKey(spec) {
|
|
1172
|
+
return spec.kind === "custom" ? `custom\0${spec.name}` : JSON.stringify(spec);
|
|
1173
|
+
}
|
|
1174
|
+
__name(memoKey, "memoKey");
|
|
1175
|
+
async function scan(rules, ctx, segments, vault = new Vault(), options = {}) {
|
|
1176
|
+
const started = performance.now();
|
|
1177
|
+
const applicable = orderRules(rules).filter((r) => ruleApplies(r, ctx));
|
|
1178
|
+
const result = {
|
|
1179
|
+
stage: ctx.stage,
|
|
1180
|
+
action: "allow",
|
|
1181
|
+
segments: segments.map((s) => s.text),
|
|
1182
|
+
changed: false,
|
|
1183
|
+
hits: [],
|
|
1184
|
+
vault,
|
|
1185
|
+
redactions: 0,
|
|
1186
|
+
latencyMs: 0
|
|
1187
|
+
};
|
|
1188
|
+
if (applicable.length === 0 || segments.length === 0) {
|
|
1189
|
+
result.latencyMs = performance.now() - started;
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
const stageMessage = options.messages?.[ctx.stage] ?? DEFAULT_MESSAGES[ctx.stage];
|
|
1193
|
+
const restorableStage = (options.restorableStages ?? [
|
|
1194
|
+
"llm_request"
|
|
1195
|
+
]).includes(ctx.stage);
|
|
1196
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1197
|
+
const detect = /* @__PURE__ */ __name((spec, index) => {
|
|
1198
|
+
const key = `${index}\0${memoKey(spec)}`;
|
|
1199
|
+
let p = memo.get(key);
|
|
1200
|
+
if (!p) {
|
|
1201
|
+
p = Promise.resolve().then(() => runDetector(spec, segments[index]?.text ?? "", ctx));
|
|
1202
|
+
memo.set(key, p);
|
|
1203
|
+
}
|
|
1204
|
+
return p;
|
|
1205
|
+
}, "detect");
|
|
1206
|
+
const outcomes = [];
|
|
1207
|
+
for (const rule of applicable) {
|
|
1208
|
+
const sources = rule.match?.sources;
|
|
1209
|
+
const indexes = segments.map((s, i) => ({
|
|
1210
|
+
s,
|
|
1211
|
+
i
|
|
1212
|
+
})).filter(({ s }) => !sources?.length || !s.source || sources.includes(s.source)).map(({ i }) => i);
|
|
1213
|
+
const outcome = {
|
|
1214
|
+
rule,
|
|
1215
|
+
bySegment: /* @__PURE__ */ new Map()
|
|
1216
|
+
};
|
|
1217
|
+
if (rule.detectors.length === 0) {
|
|
1218
|
+
if (rule.action === "allow" || rule.action === "block" || rule.action === "approve") outcomes.push(outcome);
|
|
1219
|
+
if (rule.action === "allow") break;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const jobs = rule.detectors.flatMap((spec) => indexes.map(async (i) => ({
|
|
1223
|
+
i,
|
|
1224
|
+
found: await detect(spec, i)
|
|
1225
|
+
})));
|
|
1226
|
+
const settled = await Promise.allSettled(jobs);
|
|
1227
|
+
for (const s of settled) {
|
|
1228
|
+
if (s.status === "rejected") {
|
|
1229
|
+
outcome.failed = s.reason?.message ?? String(s.reason);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (s.value.found.length === 0) continue;
|
|
1233
|
+
const list = outcome.bySegment.get(s.value.i) ?? [];
|
|
1234
|
+
list.push(...s.value.found);
|
|
1235
|
+
outcome.bySegment.set(s.value.i, list);
|
|
1236
|
+
}
|
|
1237
|
+
if (outcome.bySegment.size === 0 && !outcome.failed) continue;
|
|
1238
|
+
outcomes.push(outcome);
|
|
1239
|
+
if (rule.action === "allow" && outcome.bySegment.size > 0) break;
|
|
1240
|
+
}
|
|
1241
|
+
const redactBySegment = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const o of outcomes) {
|
|
1243
|
+
const { rule } = o;
|
|
1244
|
+
const name = ruleName(rule);
|
|
1245
|
+
const failMode = rule.options?.failMode ?? "open";
|
|
1246
|
+
let action = rule.action;
|
|
1247
|
+
if (o.failed) {
|
|
1248
|
+
result.hits.push({
|
|
1249
|
+
ruleId: rule.id,
|
|
1250
|
+
ruleName: name,
|
|
1251
|
+
action: failMode === "closed" ? "block" : "log",
|
|
1252
|
+
detector: "detector_error",
|
|
1253
|
+
category: "error.detector_unavailable",
|
|
1254
|
+
score: 0,
|
|
1255
|
+
count: 1,
|
|
1256
|
+
values: [],
|
|
1257
|
+
fresh: true,
|
|
1258
|
+
error: o.failed.slice(0, 300),
|
|
1259
|
+
failMode
|
|
1260
|
+
});
|
|
1261
|
+
if (failMode === "closed" && o.bySegment.size === 0) action = "block";
|
|
1262
|
+
else if (o.bySegment.size === 0) continue;
|
|
1263
|
+
}
|
|
1264
|
+
const historyOnly = (action === "block" || action === "approve") && o.bySegment.size > 0 && [
|
|
1265
|
+
...o.bySegment.keys()
|
|
1266
|
+
].every((i) => segments[i]?.fresh === false);
|
|
1267
|
+
if (historyOnly) {
|
|
1268
|
+
action = "redact";
|
|
1269
|
+
for (const [i, findings] of o.bySegment) {
|
|
1270
|
+
const entry = redactBySegment.get(i) ?? {
|
|
1271
|
+
findings: [],
|
|
1272
|
+
restorable: false
|
|
1273
|
+
};
|
|
1274
|
+
entry.findings.push(...findings);
|
|
1275
|
+
redactBySegment.set(i, entry);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (ACTION_SEVERITY[action] > ACTION_SEVERITY[result.action]) {
|
|
1279
|
+
result.action = action;
|
|
1280
|
+
if (action === "block" || action === "approve") {
|
|
1281
|
+
result.decisive = {
|
|
1282
|
+
id: rule.id,
|
|
1283
|
+
name,
|
|
1284
|
+
message: rule.options?.message || (o.failed && o.bySegment.size === 0 ? `${stageMessage} (a required check could not run)` : action === "approve" ? options.approvalMessage ?? DEFAULT_APPROVAL_MESSAGE : stageMessage)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (o.bySegment.size === 0 && rule.detectors.length === 0) {
|
|
1289
|
+
result.hits.push({
|
|
1290
|
+
ruleId: rule.id,
|
|
1291
|
+
ruleName: name,
|
|
1292
|
+
action,
|
|
1293
|
+
detector: "scope",
|
|
1294
|
+
category: "scope.match",
|
|
1295
|
+
score: 1,
|
|
1296
|
+
count: 1,
|
|
1297
|
+
values: [],
|
|
1298
|
+
fresh: true
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1303
|
+
for (const [i, findings] of o.bySegment) {
|
|
1304
|
+
for (const f of findings) {
|
|
1305
|
+
const hit = grouped.get(f.category) ?? {
|
|
1306
|
+
ruleId: rule.id,
|
|
1307
|
+
ruleName: name,
|
|
1308
|
+
action,
|
|
1309
|
+
detector: f.detector,
|
|
1310
|
+
category: f.category,
|
|
1311
|
+
score: 0,
|
|
1312
|
+
count: 0,
|
|
1313
|
+
values: [],
|
|
1314
|
+
fresh: false
|
|
1315
|
+
};
|
|
1316
|
+
hit.count++;
|
|
1317
|
+
hit.score = Math.max(hit.score, f.score);
|
|
1318
|
+
if (f.value && !hit.values.includes(f.value) && hit.values.length < 20) hit.values.push(f.value);
|
|
1319
|
+
if (segments[i]?.fresh !== false) hit.fresh = true;
|
|
1320
|
+
grouped.set(f.category, hit);
|
|
1321
|
+
}
|
|
1322
|
+
if (action === "redact" && !historyOnly) {
|
|
1323
|
+
const entry = redactBySegment.get(i) ?? {
|
|
1324
|
+
findings: [],
|
|
1325
|
+
restorable: false
|
|
1326
|
+
};
|
|
1327
|
+
entry.findings.push(...findings);
|
|
1328
|
+
entry.restorable ||= restorableStage && rule.options?.restore !== false;
|
|
1329
|
+
redactBySegment.set(i, entry);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
result.hits.push(...grouped.values());
|
|
1333
|
+
}
|
|
1334
|
+
if (result.action !== "block" && result.action !== "approve") {
|
|
1335
|
+
for (const [i, { findings, restorable }] of redactBySegment) {
|
|
1336
|
+
const original = segments[i]?.text ?? "";
|
|
1337
|
+
const { text, count } = applyRedactions(original, findings, vault, restorable);
|
|
1338
|
+
if (count > 0 && text !== original) {
|
|
1339
|
+
result.segments[i] = text;
|
|
1340
|
+
result.redactions += count;
|
|
1341
|
+
result.changed = true;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
result.latencyMs = performance.now() - started;
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
__name(scan, "scan");
|
|
1349
|
+
|
|
1350
|
+
// src/guardrails/segments.ts
|
|
1351
|
+
var MAX_SLOTS = 2e3;
|
|
1352
|
+
function sourceOf(role) {
|
|
1353
|
+
if (role === "system" || role === "developer") return "system";
|
|
1354
|
+
if (role === "assistant") return "assistant";
|
|
1355
|
+
if (role === "tool" || role === "function") return "tool";
|
|
1356
|
+
return "user";
|
|
1357
|
+
}
|
|
1358
|
+
__name(sourceOf, "sourceOf");
|
|
1359
|
+
function freshFrom(messages) {
|
|
1360
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "assistant") return i + 1;
|
|
1361
|
+
return 0;
|
|
1362
|
+
}
|
|
1363
|
+
__name(freshFrom, "freshFrom");
|
|
1364
|
+
function jsonSlots(root, source, fresh, replaceRoot) {
|
|
1365
|
+
const out = [];
|
|
1366
|
+
const walk = /* @__PURE__ */ __name((value, set, depth) => {
|
|
1367
|
+
if (out.length >= MAX_SLOTS || depth > 10) return;
|
|
1368
|
+
if (typeof value === "string") {
|
|
1369
|
+
if (value) out.push({
|
|
1370
|
+
segment: {
|
|
1371
|
+
text: value,
|
|
1372
|
+
source,
|
|
1373
|
+
fresh
|
|
1374
|
+
},
|
|
1375
|
+
set
|
|
1376
|
+
});
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (Array.isArray(value)) {
|
|
1380
|
+
for (let i = 0; i < value.length; i++) {
|
|
1381
|
+
walk(value[i], (t) => {
|
|
1382
|
+
value[i] = t;
|
|
1383
|
+
}, depth + 1);
|
|
1384
|
+
}
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (value && typeof value === "object") {
|
|
1388
|
+
const o = value;
|
|
1389
|
+
for (const k of Object.keys(o)) walk(o[k], (t) => {
|
|
1390
|
+
o[k] = t;
|
|
1391
|
+
}, depth + 1);
|
|
1392
|
+
}
|
|
1393
|
+
}, "walk");
|
|
1394
|
+
walk(root, replaceRoot ?? (() => void 0), 0);
|
|
1395
|
+
return out;
|
|
1396
|
+
}
|
|
1397
|
+
__name(jsonSlots, "jsonSlots");
|
|
1398
|
+
function jsonStringSlot(holder, key, source, fresh) {
|
|
1399
|
+
const v = holder[key];
|
|
1400
|
+
if (typeof v !== "string" || !v) return [];
|
|
1401
|
+
return [
|
|
1402
|
+
{
|
|
1403
|
+
segment: {
|
|
1404
|
+
text: v,
|
|
1405
|
+
source,
|
|
1406
|
+
fresh
|
|
1407
|
+
},
|
|
1408
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1409
|
+
holder[key] = t;
|
|
1410
|
+
}, "set"),
|
|
1411
|
+
json: true
|
|
1412
|
+
}
|
|
1413
|
+
];
|
|
1414
|
+
}
|
|
1415
|
+
__name(jsonStringSlot, "jsonStringSlot");
|
|
1416
|
+
function contentSlots(holder, source, fresh) {
|
|
1417
|
+
const content = holder.content;
|
|
1418
|
+
if (typeof content === "string") {
|
|
1419
|
+
return content ? [
|
|
1420
|
+
{
|
|
1421
|
+
segment: {
|
|
1422
|
+
text: content,
|
|
1423
|
+
source,
|
|
1424
|
+
fresh
|
|
1425
|
+
},
|
|
1426
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1427
|
+
holder.content = t;
|
|
1428
|
+
}, "set")
|
|
1429
|
+
}
|
|
1430
|
+
] : [];
|
|
1431
|
+
}
|
|
1432
|
+
if (!Array.isArray(content)) return [];
|
|
1433
|
+
const out = [];
|
|
1434
|
+
for (const part of content) {
|
|
1435
|
+
if (!part || typeof part !== "object") continue;
|
|
1436
|
+
const p = part;
|
|
1437
|
+
if ((p.type === "text" || p.type === "input_text" || p.type === "output_text") && typeof p.text === "string") {
|
|
1438
|
+
out.push({
|
|
1439
|
+
segment: {
|
|
1440
|
+
text: p.text,
|
|
1441
|
+
source,
|
|
1442
|
+
fresh
|
|
1443
|
+
},
|
|
1444
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1445
|
+
p.text = t;
|
|
1446
|
+
}, "set")
|
|
1447
|
+
});
|
|
1448
|
+
} else if (p.type === "tool_result") {
|
|
1449
|
+
out.push(...contentSlots(p, "tool", fresh));
|
|
1450
|
+
} else if (p.type === "tool_use" && p.input && typeof p.input === "object") {
|
|
1451
|
+
out.push(...jsonSlots(p.input, "assistant", fresh));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return out;
|
|
1455
|
+
}
|
|
1456
|
+
__name(contentSlots, "contentSlots");
|
|
1457
|
+
function requestSlots(api, body) {
|
|
1458
|
+
const out = [];
|
|
1459
|
+
if (api === "anthropic") {
|
|
1460
|
+
if (typeof body.system === "string" && body.system) {
|
|
1461
|
+
out.push({
|
|
1462
|
+
segment: {
|
|
1463
|
+
text: body.system,
|
|
1464
|
+
source: "system",
|
|
1465
|
+
fresh: true
|
|
1466
|
+
},
|
|
1467
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1468
|
+
body.system = t;
|
|
1469
|
+
}, "set")
|
|
1470
|
+
});
|
|
1471
|
+
} else if (Array.isArray(body.system)) {
|
|
1472
|
+
out.push(...contentSlots({
|
|
1473
|
+
content: body.system
|
|
1474
|
+
}, "system", true));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (Array.isArray(body.messages)) {
|
|
1478
|
+
const messages = body.messages;
|
|
1479
|
+
const from = freshFrom(messages);
|
|
1480
|
+
messages.forEach((m, i) => {
|
|
1481
|
+
if (!m || typeof m !== "object") return;
|
|
1482
|
+
const fresh = i >= from || m.role === "system";
|
|
1483
|
+
const source = sourceOf(m.role);
|
|
1484
|
+
out.push(...contentSlots(m, source, fresh));
|
|
1485
|
+
if (Array.isArray(m.tool_calls)) {
|
|
1486
|
+
for (const call of m.tool_calls) if (call?.function) out.push(...jsonStringSlot(call.function, "arguments", "assistant", fresh));
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof body.input === "string" || Array.isArray(body.input)) {
|
|
1491
|
+
const holder = body;
|
|
1492
|
+
if (typeof holder.input === "string") {
|
|
1493
|
+
out.push({
|
|
1494
|
+
segment: {
|
|
1495
|
+
text: holder.input,
|
|
1496
|
+
source: "user",
|
|
1497
|
+
fresh: true
|
|
1498
|
+
},
|
|
1499
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1500
|
+
holder.input = t;
|
|
1501
|
+
}, "set")
|
|
1502
|
+
});
|
|
1503
|
+
} else {
|
|
1504
|
+
const list = holder.input;
|
|
1505
|
+
list.forEach((v, i) => {
|
|
1506
|
+
if (typeof v === "string" && v) out.push({
|
|
1507
|
+
segment: {
|
|
1508
|
+
text: v,
|
|
1509
|
+
source: "user",
|
|
1510
|
+
fresh: true
|
|
1511
|
+
},
|
|
1512
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1513
|
+
list[i] = t;
|
|
1514
|
+
}, "set")
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return out.slice(0, MAX_SLOTS);
|
|
1520
|
+
}
|
|
1521
|
+
__name(requestSlots, "requestSlots");
|
|
1522
|
+
function responseSlots(api, body) {
|
|
1523
|
+
if (!body || typeof body !== "object") return [];
|
|
1524
|
+
const b = body;
|
|
1525
|
+
const out = [];
|
|
1526
|
+
if (api === "anthropic") {
|
|
1527
|
+
if (Array.isArray(b.content)) out.push(...contentSlots(b, "assistant", true));
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
for (const choice of Array.isArray(b.choices) ? b.choices : []) {
|
|
1531
|
+
const msg = choice?.message;
|
|
1532
|
+
if (!msg || typeof msg !== "object") continue;
|
|
1533
|
+
out.push(...contentSlots(msg, "assistant", true));
|
|
1534
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
1535
|
+
for (const call of msg.tool_calls) if (call?.function) out.push(...jsonStringSlot(call.function, "arguments", "assistant", true));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return out;
|
|
1539
|
+
}
|
|
1540
|
+
__name(responseSlots, "responseSlots");
|
|
1541
|
+
function refuseResponse(api, body, message) {
|
|
1542
|
+
if (!body || typeof body !== "object") return body;
|
|
1543
|
+
const b = body;
|
|
1544
|
+
if (api === "anthropic") {
|
|
1545
|
+
return {
|
|
1546
|
+
...b,
|
|
1547
|
+
content: [
|
|
1548
|
+
{
|
|
1549
|
+
type: "text",
|
|
1550
|
+
text: message
|
|
1551
|
+
}
|
|
1552
|
+
],
|
|
1553
|
+
stop_reason: "refusal"
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
return {
|
|
1557
|
+
...b,
|
|
1558
|
+
choices: (Array.isArray(b.choices) && b.choices.length > 0 ? b.choices : [
|
|
1559
|
+
{
|
|
1560
|
+
index: 0
|
|
1561
|
+
}
|
|
1562
|
+
]).map((c) => ({
|
|
1563
|
+
...c,
|
|
1564
|
+
message: {
|
|
1565
|
+
role: "assistant",
|
|
1566
|
+
content: message
|
|
1567
|
+
},
|
|
1568
|
+
finish_reason: "content_filter"
|
|
1569
|
+
}))
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
__name(refuseResponse, "refuseResponse");
|
|
1573
|
+
function toolResultSlots(result) {
|
|
1574
|
+
const out = [];
|
|
1575
|
+
if (Array.isArray(result.content)) {
|
|
1576
|
+
for (const item of result.content) {
|
|
1577
|
+
if (!item || typeof item !== "object") continue;
|
|
1578
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1579
|
+
out.push({
|
|
1580
|
+
segment: {
|
|
1581
|
+
text: item.text,
|
|
1582
|
+
source: "tool",
|
|
1583
|
+
fresh: true
|
|
1584
|
+
},
|
|
1585
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1586
|
+
item.text = t;
|
|
1587
|
+
}, "set")
|
|
1588
|
+
});
|
|
1589
|
+
} else if (item.type === "resource" && item.resource && typeof item.resource.text === "string") {
|
|
1590
|
+
out.push({
|
|
1591
|
+
segment: {
|
|
1592
|
+
text: item.resource.text,
|
|
1593
|
+
source: "tool",
|
|
1594
|
+
fresh: true
|
|
1595
|
+
},
|
|
1596
|
+
set: /* @__PURE__ */ __name((t) => {
|
|
1597
|
+
item.resource.text = t;
|
|
1598
|
+
}, "set")
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (result.structuredContent && typeof result.structuredContent === "object") out.push(...jsonSlots(result.structuredContent, "tool", true));
|
|
1604
|
+
return out.slice(0, MAX_SLOTS);
|
|
1605
|
+
}
|
|
1606
|
+
__name(toolResultSlots, "toolResultSlots");
|
|
1607
|
+
|
|
1608
|
+
// src/guardrails/stream-guard.ts
|
|
1609
|
+
var Channel = class Channel2 {
|
|
1610
|
+
static {
|
|
1611
|
+
__name(this, "Channel");
|
|
1612
|
+
}
|
|
1613
|
+
json;
|
|
1614
|
+
pending = "";
|
|
1615
|
+
constructor(json) {
|
|
1616
|
+
this.json = json;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
var StreamGuard = class {
|
|
1620
|
+
static {
|
|
1621
|
+
__name(this, "StreamGuard");
|
|
1622
|
+
}
|
|
1623
|
+
opts;
|
|
1624
|
+
buffer = "";
|
|
1625
|
+
decoder = new TextDecoder();
|
|
1626
|
+
channels = /* @__PURE__ */ new Map();
|
|
1627
|
+
window;
|
|
1628
|
+
holdback;
|
|
1629
|
+
blocked;
|
|
1630
|
+
template;
|
|
1631
|
+
openBlocks = /* @__PURE__ */ new Set();
|
|
1632
|
+
maxBlock = -1;
|
|
1633
|
+
done = false;
|
|
1634
|
+
constructor(opts) {
|
|
1635
|
+
this.opts = opts;
|
|
1636
|
+
this.window = opts.window ?? 256;
|
|
1637
|
+
this.holdback = opts.holdback ?? 64;
|
|
1638
|
+
}
|
|
1639
|
+
/** Whether the guard changes anything (else the caller can pass bytes straight through). */
|
|
1640
|
+
get active() {
|
|
1641
|
+
return !!this.opts.scan || this.opts.restore && this.opts.vault.restorableCount > 0;
|
|
1642
|
+
}
|
|
1643
|
+
async push(chunk) {
|
|
1644
|
+
this.buffer += typeof chunk === "string" ? chunk : this.decoder.decode(chunk, {
|
|
1645
|
+
stream: true
|
|
1646
|
+
});
|
|
1647
|
+
let out = "";
|
|
1648
|
+
for (; ; ) {
|
|
1649
|
+
const m = /\r?\n\r?\n/.exec(this.buffer);
|
|
1650
|
+
if (!m) break;
|
|
1651
|
+
const raw = this.buffer.slice(0, m.index + m[0].length);
|
|
1652
|
+
this.buffer = this.buffer.slice(m.index + m[0].length);
|
|
1653
|
+
out += await this.frame(parseFrame(raw));
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1657
|
+
async end() {
|
|
1658
|
+
let out = "";
|
|
1659
|
+
if (this.buffer.trim()) out += await this.frame(parseFrame(`${this.buffer}
|
|
1660
|
+
|
|
1661
|
+
`));
|
|
1662
|
+
this.buffer = "";
|
|
1663
|
+
if (!this.blocked && !this.done) out += await this.flushAll();
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
async frame(f) {
|
|
1667
|
+
if (this.blocked) return "";
|
|
1668
|
+
if (this.opts.api === "openai") return this.openaiFrame(f);
|
|
1669
|
+
return this.anthropicFrame(f);
|
|
1670
|
+
}
|
|
1671
|
+
// ---- OpenAI chat-completion chunks ---------------------------------------------------------
|
|
1672
|
+
async openaiFrame(f) {
|
|
1673
|
+
if (!f.data) return f.raw;
|
|
1674
|
+
if (f.data.trim() === "[DONE]") {
|
|
1675
|
+
const tail = await this.flushAll();
|
|
1676
|
+
this.done = true;
|
|
1677
|
+
return tail + f.raw;
|
|
1678
|
+
}
|
|
1679
|
+
let chunk;
|
|
1680
|
+
try {
|
|
1681
|
+
chunk = JSON.parse(f.data);
|
|
1682
|
+
} catch {
|
|
1683
|
+
return f.raw;
|
|
1684
|
+
}
|
|
1685
|
+
this.template ??= {
|
|
1686
|
+
id: chunk.id,
|
|
1687
|
+
object: chunk.object,
|
|
1688
|
+
created: chunk.created,
|
|
1689
|
+
model: chunk.model
|
|
1690
|
+
};
|
|
1691
|
+
if (!Array.isArray(chunk.choices) || chunk.choices.length === 0) return f.raw;
|
|
1692
|
+
let prefix = "";
|
|
1693
|
+
let keep = false;
|
|
1694
|
+
for (const choice of chunk.choices) {
|
|
1695
|
+
const delta = choice.delta ?? {};
|
|
1696
|
+
const ci = choice.index ?? 0;
|
|
1697
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
1698
|
+
const r = await this.feed(`c:${ci}`, delta.content, false);
|
|
1699
|
+
if (this.blocked) return prefix + this.openaiBlock(ci);
|
|
1700
|
+
delta.content = r;
|
|
1701
|
+
if (r) keep = true;
|
|
1702
|
+
}
|
|
1703
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
1704
|
+
for (const call of delta.tool_calls) {
|
|
1705
|
+
const fn = call.function;
|
|
1706
|
+
if (fn && typeof fn.arguments === "string" && fn.arguments) {
|
|
1707
|
+
const r = await this.feed(`t:${ci}:${call.index ?? 0}`, fn.arguments, true);
|
|
1708
|
+
if (this.blocked) return prefix + this.openaiBlock(ci);
|
|
1709
|
+
fn.arguments = r;
|
|
1710
|
+
}
|
|
1711
|
+
if (call.id || fn?.name || fn?.arguments) keep = true;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (delta.role || delta.reasoning_content || delta.refusal) keep = true;
|
|
1715
|
+
if (choice.finish_reason) {
|
|
1716
|
+
prefix += await this.flushChoice(ci);
|
|
1717
|
+
if (this.blocked) return prefix + this.openaiBlock(ci);
|
|
1718
|
+
keep = true;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (chunk.usage) keep = true;
|
|
1722
|
+
return keep ? `${prefix}data: ${JSON.stringify(chunk)}
|
|
1723
|
+
|
|
1724
|
+
` : prefix;
|
|
1725
|
+
}
|
|
1726
|
+
openaiChunk(ci, delta, finish = null) {
|
|
1727
|
+
return `data: ${JSON.stringify({
|
|
1728
|
+
...this.template ?? {
|
|
1729
|
+
object: "chat.completion.chunk"
|
|
1730
|
+
},
|
|
1731
|
+
choices: [
|
|
1732
|
+
{
|
|
1733
|
+
index: ci,
|
|
1734
|
+
delta,
|
|
1735
|
+
finish_reason: finish
|
|
1736
|
+
}
|
|
1737
|
+
]
|
|
1738
|
+
})}
|
|
1739
|
+
|
|
1740
|
+
`;
|
|
1741
|
+
}
|
|
1742
|
+
openaiBlock(ci) {
|
|
1743
|
+
const msg = this.blocked?.message ?? "";
|
|
1744
|
+
this.done = true;
|
|
1745
|
+
return `${this.openaiChunk(ci, {
|
|
1746
|
+
content: `
|
|
1747
|
+
|
|
1748
|
+
${msg}`
|
|
1749
|
+
})}${this.openaiChunk(ci, {}, "content_filter")}data: [DONE]
|
|
1750
|
+
|
|
1751
|
+
`;
|
|
1752
|
+
}
|
|
1753
|
+
async flushChoice(ci) {
|
|
1754
|
+
let out = "";
|
|
1755
|
+
for (const [key, ch] of this.channels) {
|
|
1756
|
+
if (!key.startsWith(`c:${ci}`) && !key.startsWith(`t:${ci}:`)) continue;
|
|
1757
|
+
const text = await this.release(key, ch, true);
|
|
1758
|
+
if (this.blocked) return out;
|
|
1759
|
+
if (!text) continue;
|
|
1760
|
+
out += key.startsWith("c:") ? this.openaiChunk(ci, {
|
|
1761
|
+
content: text
|
|
1762
|
+
}) : this.openaiChunk(ci, {
|
|
1763
|
+
tool_calls: [
|
|
1764
|
+
{
|
|
1765
|
+
index: Number(key.split(":")[2]),
|
|
1766
|
+
function: {
|
|
1767
|
+
arguments: text
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
]
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
return out;
|
|
1774
|
+
}
|
|
1775
|
+
// ---- Anthropic Messages events -------------------------------------------------------------
|
|
1776
|
+
async anthropicFrame(f) {
|
|
1777
|
+
if (!f.data) return f.raw;
|
|
1778
|
+
let ev;
|
|
1779
|
+
try {
|
|
1780
|
+
ev = JSON.parse(f.data);
|
|
1781
|
+
} catch {
|
|
1782
|
+
return f.raw;
|
|
1783
|
+
}
|
|
1784
|
+
const type = ev.type ?? f.event;
|
|
1785
|
+
if (type === "content_block_start") {
|
|
1786
|
+
this.openBlocks.add(ev.index);
|
|
1787
|
+
this.maxBlock = Math.max(this.maxBlock, ev.index);
|
|
1788
|
+
return f.raw;
|
|
1789
|
+
}
|
|
1790
|
+
if (type === "content_block_delta") {
|
|
1791
|
+
const d = ev.delta ?? {};
|
|
1792
|
+
if (d.type === "text_delta" && typeof d.text === "string") {
|
|
1793
|
+
const r = await this.feed(`b:${ev.index}`, d.text, false);
|
|
1794
|
+
if (this.blocked) return this.anthropicBlock(ev.index);
|
|
1795
|
+
if (!r) return "";
|
|
1796
|
+
d.text = r;
|
|
1797
|
+
return sseEvent("content_block_delta", ev);
|
|
1798
|
+
}
|
|
1799
|
+
if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
|
|
1800
|
+
const r = await this.feed(`b:${ev.index}`, d.partial_json, true);
|
|
1801
|
+
if (this.blocked) return this.anthropicBlock(ev.index);
|
|
1802
|
+
if (!r) return "";
|
|
1803
|
+
d.partial_json = r;
|
|
1804
|
+
return sseEvent("content_block_delta", ev);
|
|
1805
|
+
}
|
|
1806
|
+
return f.raw;
|
|
1807
|
+
}
|
|
1808
|
+
if (type === "content_block_stop") {
|
|
1809
|
+
const pre = await this.flushBlock(ev.index);
|
|
1810
|
+
if (this.blocked) return pre + this.anthropicBlock(ev.index);
|
|
1811
|
+
this.openBlocks.delete(ev.index);
|
|
1812
|
+
return pre + f.raw;
|
|
1813
|
+
}
|
|
1814
|
+
if (type === "message_delta" || type === "message_stop") {
|
|
1815
|
+
let pre = "";
|
|
1816
|
+
for (const i of [
|
|
1817
|
+
...this.openBlocks
|
|
1818
|
+
]) {
|
|
1819
|
+
pre += await this.flushBlock(i);
|
|
1820
|
+
if (this.blocked) return pre + this.anthropicBlock(i);
|
|
1821
|
+
}
|
|
1822
|
+
if (type === "message_stop") this.done = true;
|
|
1823
|
+
return pre + f.raw;
|
|
1824
|
+
}
|
|
1825
|
+
return f.raw;
|
|
1826
|
+
}
|
|
1827
|
+
async flushBlock(index) {
|
|
1828
|
+
const key = `b:${index}`;
|
|
1829
|
+
const ch = this.channels.get(key);
|
|
1830
|
+
if (!ch) return "";
|
|
1831
|
+
const text = await this.release(key, ch, true);
|
|
1832
|
+
if (this.blocked || !text) return "";
|
|
1833
|
+
return sseEvent("content_block_delta", {
|
|
1834
|
+
type: "content_block_delta",
|
|
1835
|
+
index,
|
|
1836
|
+
delta: ch.json ? {
|
|
1837
|
+
type: "input_json_delta",
|
|
1838
|
+
partial_json: text
|
|
1839
|
+
} : {
|
|
1840
|
+
type: "text_delta",
|
|
1841
|
+
text
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
anthropicBlock(index) {
|
|
1846
|
+
const msg = this.blocked?.message ?? "";
|
|
1847
|
+
this.done = true;
|
|
1848
|
+
let out = "";
|
|
1849
|
+
let i = index;
|
|
1850
|
+
const ch = this.channels.get(`b:${index}`);
|
|
1851
|
+
if (!this.openBlocks.has(index) || ch?.json) {
|
|
1852
|
+
if (this.openBlocks.has(index)) out += sseEvent("content_block_stop", {
|
|
1853
|
+
type: "content_block_stop",
|
|
1854
|
+
index
|
|
1855
|
+
});
|
|
1856
|
+
i = this.maxBlock + 1;
|
|
1857
|
+
out += sseEvent("content_block_start", {
|
|
1858
|
+
type: "content_block_start",
|
|
1859
|
+
index: i,
|
|
1860
|
+
content_block: {
|
|
1861
|
+
type: "text",
|
|
1862
|
+
text: ""
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
out += sseEvent("content_block_delta", {
|
|
1867
|
+
type: "content_block_delta",
|
|
1868
|
+
index: i,
|
|
1869
|
+
delta: {
|
|
1870
|
+
type: "text_delta",
|
|
1871
|
+
text: `
|
|
1872
|
+
|
|
1873
|
+
${msg}`
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
out += sseEvent("content_block_stop", {
|
|
1877
|
+
type: "content_block_stop",
|
|
1878
|
+
index: i
|
|
1879
|
+
});
|
|
1880
|
+
out += sseEvent("message_delta", {
|
|
1881
|
+
type: "message_delta",
|
|
1882
|
+
delta: {
|
|
1883
|
+
stop_reason: "refusal",
|
|
1884
|
+
stop_sequence: null
|
|
1885
|
+
},
|
|
1886
|
+
usage: {
|
|
1887
|
+
output_tokens: 0
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
out += sseEvent("message_stop", {
|
|
1891
|
+
type: "message_stop"
|
|
1892
|
+
});
|
|
1893
|
+
return out;
|
|
1894
|
+
}
|
|
1895
|
+
// ---- windows -------------------------------------------------------------------------------
|
|
1896
|
+
async flushAll() {
|
|
1897
|
+
if (this.opts.api === "openai") {
|
|
1898
|
+
const choices = new Set([
|
|
1899
|
+
...this.channels.keys()
|
|
1900
|
+
].map((k) => Number(k.split(":")[1])));
|
|
1901
|
+
let out2 = "";
|
|
1902
|
+
for (const ci of choices) {
|
|
1903
|
+
out2 += await this.flushChoice(ci);
|
|
1904
|
+
if (this.blocked) return out2 + this.openaiBlock(ci);
|
|
1905
|
+
}
|
|
1906
|
+
return out2;
|
|
1907
|
+
}
|
|
1908
|
+
let out = "";
|
|
1909
|
+
for (const i of [
|
|
1910
|
+
...this.openBlocks
|
|
1911
|
+
]) {
|
|
1912
|
+
out += await this.flushBlock(i);
|
|
1913
|
+
if (this.blocked) return out + this.anthropicBlock(i);
|
|
1914
|
+
}
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
async feed(key, text, json) {
|
|
1918
|
+
let ch = this.channels.get(key);
|
|
1919
|
+
if (!ch) {
|
|
1920
|
+
ch = new Channel(json);
|
|
1921
|
+
this.channels.set(key, ch);
|
|
1922
|
+
}
|
|
1923
|
+
ch.pending += text;
|
|
1924
|
+
if (ch.pending.length < this.window + this.holdback) return "";
|
|
1925
|
+
return this.release(key, ch, false);
|
|
1926
|
+
}
|
|
1927
|
+
/** Releases a window (everything when `all`), scanned and restored. */
|
|
1928
|
+
async release(key, ch, all) {
|
|
1929
|
+
if (!ch.pending) return "";
|
|
1930
|
+
let cut = all ? ch.pending.length : ch.pending.length - this.holdback;
|
|
1931
|
+
if (!all) {
|
|
1932
|
+
const spans = resolveOverlaps(this.opts.locate?.(ch.pending) ?? []);
|
|
1933
|
+
const straddling = spans.find((f) => f.start < cut && f.end > cut);
|
|
1934
|
+
if (straddling) cut = straddling.end < ch.pending.length ? straddling.end : straddling.start;
|
|
1935
|
+
const head = ch.pending.slice(0, cut);
|
|
1936
|
+
cut -= this.opts.vault.pendingPrefix(head);
|
|
1937
|
+
if (cut <= 0) return "";
|
|
1938
|
+
}
|
|
1939
|
+
let text = ch.pending.slice(0, cut);
|
|
1940
|
+
ch.pending = ch.pending.slice(cut);
|
|
1941
|
+
if (this.opts.scan) {
|
|
1942
|
+
const verdict = await this.opts.scan(text, key);
|
|
1943
|
+
if (verdict.block) {
|
|
1944
|
+
this.blocked = verdict.block;
|
|
1945
|
+
return "";
|
|
1946
|
+
}
|
|
1947
|
+
text = verdict.text;
|
|
1948
|
+
}
|
|
1949
|
+
return this.opts.restore ? this.opts.vault.restore(text, ch.json) : text;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
function parseFrame(raw) {
|
|
1953
|
+
let event;
|
|
1954
|
+
const data = [];
|
|
1955
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1956
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
1957
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
event,
|
|
1961
|
+
data: data.join("\n"),
|
|
1962
|
+
raw
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
__name(parseFrame, "parseFrame");
|
|
1966
|
+
function sseEvent(event, data) {
|
|
1967
|
+
return `event: ${event}
|
|
1968
|
+
data: ${JSON.stringify(data)}
|
|
1969
|
+
|
|
1970
|
+
`;
|
|
1971
|
+
}
|
|
1972
|
+
__name(sseEvent, "sseEvent");
|
|
1973
|
+
|
|
1974
|
+
// src/guardrails/agent.ts
|
|
1975
|
+
var InMemoryVaultStore = class {
|
|
1976
|
+
static {
|
|
1977
|
+
__name(this, "InMemoryVaultStore");
|
|
1978
|
+
}
|
|
1979
|
+
maxThreads;
|
|
1980
|
+
vaults = /* @__PURE__ */ new Map();
|
|
1981
|
+
constructor(maxThreads = 1e3) {
|
|
1982
|
+
this.maxThreads = maxThreads;
|
|
1983
|
+
}
|
|
1984
|
+
load(threadId) {
|
|
1985
|
+
const vault = this.vaults.get(threadId);
|
|
1986
|
+
if (vault !== void 0) {
|
|
1987
|
+
this.vaults.delete(threadId);
|
|
1988
|
+
this.vaults.set(threadId, vault);
|
|
1989
|
+
}
|
|
1990
|
+
return vault;
|
|
1991
|
+
}
|
|
1992
|
+
save(threadId, vault) {
|
|
1993
|
+
this.vaults.delete(threadId);
|
|
1994
|
+
this.vaults.set(threadId, vault);
|
|
1995
|
+
while (this.vaults.size > this.maxThreads) {
|
|
1996
|
+
const oldest = this.vaults.keys().next().value;
|
|
1997
|
+
if (oldest === void 0) break;
|
|
1998
|
+
this.vaults.delete(oldest);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
var GuardrailBlockedError = class extends Error {
|
|
2003
|
+
static {
|
|
2004
|
+
__name(this, "GuardrailBlockedError");
|
|
2005
|
+
}
|
|
2006
|
+
stage;
|
|
2007
|
+
userMessage;
|
|
2008
|
+
rule;
|
|
2009
|
+
constructor(stage, userMessage, rule) {
|
|
2010
|
+
super(userMessage), this.stage = stage, this.userMessage = userMessage, this.rule = rule;
|
|
2011
|
+
this.name = "GuardrailBlockedError";
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
var ALL_CONTENT = [
|
|
2015
|
+
"llm_request",
|
|
2016
|
+
"tool_args",
|
|
2017
|
+
"tool_result",
|
|
2018
|
+
"llm_response"
|
|
2019
|
+
];
|
|
2020
|
+
var RESTORABLE_STAGES = [
|
|
2021
|
+
"llm_request",
|
|
2022
|
+
"tool_result"
|
|
2023
|
+
];
|
|
2024
|
+
var PII_STAGES = [
|
|
2025
|
+
"llm_request",
|
|
2026
|
+
"tool_result",
|
|
2027
|
+
"llm_response"
|
|
2028
|
+
];
|
|
2029
|
+
function shorthandRules(options) {
|
|
2030
|
+
const rules = [];
|
|
2031
|
+
if (options.pii !== void 0) {
|
|
2032
|
+
const pii = typeof options.pii === "string" ? {
|
|
2033
|
+
action: options.pii
|
|
2034
|
+
} : options.pii;
|
|
2035
|
+
rules.push({
|
|
2036
|
+
id: "pii",
|
|
2037
|
+
name: "PII",
|
|
2038
|
+
stages: pii.stages ?? PII_STAGES,
|
|
2039
|
+
detectors: [
|
|
2040
|
+
{
|
|
2041
|
+
kind: "pii",
|
|
2042
|
+
...pii.types !== void 0 ? {
|
|
2043
|
+
types: pii.types
|
|
2044
|
+
} : {}
|
|
2045
|
+
}
|
|
2046
|
+
],
|
|
2047
|
+
action: pii.action,
|
|
2048
|
+
options: {
|
|
2049
|
+
restore: pii.restore ?? true
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
if (options.secrets !== void 0) {
|
|
2054
|
+
const secrets = typeof options.secrets === "string" ? {
|
|
2055
|
+
action: options.secrets
|
|
2056
|
+
} : options.secrets;
|
|
2057
|
+
rules.push({
|
|
2058
|
+
id: "secrets",
|
|
2059
|
+
name: "Secrets",
|
|
2060
|
+
stages: secrets.stages ?? ALL_CONTENT,
|
|
2061
|
+
detectors: [
|
|
2062
|
+
{
|
|
2063
|
+
kind: "secrets",
|
|
2064
|
+
...secrets.types !== void 0 ? {
|
|
2065
|
+
types: secrets.types
|
|
2066
|
+
} : {}
|
|
2067
|
+
}
|
|
2068
|
+
],
|
|
2069
|
+
action: secrets.action,
|
|
2070
|
+
// A secret sent to a model is not handed back: the reader should not see it echoed either.
|
|
2071
|
+
options: {
|
|
2072
|
+
restore: false
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
if (options.injection !== void 0) {
|
|
2077
|
+
const injection = typeof options.injection === "string" ? {
|
|
2078
|
+
action: options.injection
|
|
2079
|
+
} : options.injection;
|
|
2080
|
+
rules.push({
|
|
2081
|
+
id: "injection",
|
|
2082
|
+
name: "Prompt injection",
|
|
2083
|
+
// The prompt (a jailbreak typed by the user) and tool results (indirect injection).
|
|
2084
|
+
stages: injection.stages ?? [
|
|
2085
|
+
"llm_request",
|
|
2086
|
+
"tool_result"
|
|
2087
|
+
],
|
|
2088
|
+
detectors: [
|
|
2089
|
+
{
|
|
2090
|
+
kind: "injection",
|
|
2091
|
+
...injection.threshold !== void 0 ? {
|
|
2092
|
+
threshold: injection.threshold
|
|
2093
|
+
} : {}
|
|
2094
|
+
}
|
|
2095
|
+
],
|
|
2096
|
+
action: injection.action ?? "block",
|
|
2097
|
+
// What the user typed and what tools returned. Not the system prompt — the app's own text,
|
|
2098
|
+
// which legitimately carries role tags and imperatives — nor the model's earlier answers.
|
|
2099
|
+
match: {
|
|
2100
|
+
sources: [
|
|
2101
|
+
"user",
|
|
2102
|
+
"tool"
|
|
2103
|
+
]
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
if (options.toolPoisoning !== void 0 && options.toolPoisoning !== false) {
|
|
2108
|
+
const poisoning = options.toolPoisoning === true ? {} : options.toolPoisoning;
|
|
2109
|
+
rules.push({
|
|
2110
|
+
id: "tool_poisoning",
|
|
2111
|
+
name: "Tool poisoning",
|
|
2112
|
+
stages: [
|
|
2113
|
+
"tool_description"
|
|
2114
|
+
],
|
|
2115
|
+
detectors: [
|
|
2116
|
+
{
|
|
2117
|
+
kind: "tool_poisoning",
|
|
2118
|
+
...poisoning.threshold !== void 0 ? {
|
|
2119
|
+
threshold: poisoning.threshold
|
|
2120
|
+
} : {}
|
|
2121
|
+
}
|
|
2122
|
+
],
|
|
2123
|
+
action: poisoning.action ?? "block"
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
return rules;
|
|
2127
|
+
}
|
|
2128
|
+
__name(shorthandRules, "shorthandRules");
|
|
2129
|
+
var isRefusal = /* @__PURE__ */ __name((action) => action === "block" || action === "approve", "isRefusal");
|
|
2130
|
+
function sourceOf2(role) {
|
|
2131
|
+
return role === "system" ? "system" : role === "assistant" ? "assistant" : "user";
|
|
2132
|
+
}
|
|
2133
|
+
__name(sourceOf2, "sourceOf");
|
|
2134
|
+
function turnStart(messages) {
|
|
2135
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2136
|
+
const message = messages[i];
|
|
2137
|
+
if (message?.role === "user" && message.content) return i;
|
|
2138
|
+
}
|
|
2139
|
+
return 0;
|
|
2140
|
+
}
|
|
2141
|
+
__name(turnStart, "turnStart");
|
|
2142
|
+
function cloneJson(value) {
|
|
2143
|
+
return value === void 0 ? value : structuredClone(value);
|
|
2144
|
+
}
|
|
2145
|
+
__name(cloneJson, "cloneJson");
|
|
2146
|
+
function writeBack(slots, result) {
|
|
2147
|
+
if (!result.changed) return;
|
|
2148
|
+
slots.forEach((slot, i) => {
|
|
2149
|
+
const text = result.segments[i];
|
|
2150
|
+
if (text !== void 0 && text !== slot.segment.text) slot.set(text);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
__name(writeBack, "writeBack");
|
|
2154
|
+
var Guardrails = class {
|
|
2155
|
+
static {
|
|
2156
|
+
__name(this, "Guardrails");
|
|
2157
|
+
}
|
|
2158
|
+
options;
|
|
2159
|
+
/** Scans every prompt; redacts in place, withholds tool results, throws on a blocked prompt. */
|
|
2160
|
+
input;
|
|
2161
|
+
/** Scans every answer and the tool calls it asks for; redacts, restores placeholders, or rejects. */
|
|
2162
|
+
output;
|
|
2163
|
+
shorthand;
|
|
2164
|
+
vaults;
|
|
2165
|
+
/** Per thread: hits already reported this turn, so re-scans of the same content stay quiet. */
|
|
2166
|
+
reported = /* @__PURE__ */ new Map();
|
|
2167
|
+
constructor(options = {}) {
|
|
2168
|
+
this.options = options;
|
|
2169
|
+
this.shorthand = shorthandRules(options);
|
|
2170
|
+
this.vaults = options.vaults ?? new InMemoryVaultStore();
|
|
2171
|
+
const name = options.name ?? "guardrails";
|
|
2172
|
+
this.input = {
|
|
2173
|
+
name,
|
|
2174
|
+
process: /* @__PURE__ */ __name((prompt, ctx) => this.processInput(prompt, ctx), "process")
|
|
2175
|
+
};
|
|
2176
|
+
const incremental = options.incremental === false ? void 0 : {
|
|
2177
|
+
lookbackChars: options.incremental?.lookbackChars ?? 256
|
|
2178
|
+
};
|
|
2179
|
+
this.output = {
|
|
2180
|
+
name,
|
|
2181
|
+
...incremental !== void 0 ? {
|
|
2182
|
+
incremental
|
|
2183
|
+
} : {},
|
|
2184
|
+
process: /* @__PURE__ */ __name((answer, ctx) => this.processOutput(answer, ctx), "process")
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
/** Every rule in force for `ctx`: the shorthands, then {@link GuardrailsOptions.rules}. */
|
|
2188
|
+
async rulesFor(ctx) {
|
|
2189
|
+
const extra = this.options.rules;
|
|
2190
|
+
const resolved = typeof extra === "function" ? await extra(ctx) : extra ?? [];
|
|
2191
|
+
return [
|
|
2192
|
+
...this.shorthand,
|
|
2193
|
+
...resolved
|
|
2194
|
+
];
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Scans arbitrary segments at one stage with the configured rules, reporting through `onEvent`.
|
|
2198
|
+
* What the processors use; also the entry point for traffic that never meets the loop.
|
|
2199
|
+
*/
|
|
2200
|
+
async scan(ctx, segments, vault = new Vault()) {
|
|
2201
|
+
const rules = await this.rulesFor(ctx);
|
|
2202
|
+
const result = await scan(rules, ctx, segments, vault, {
|
|
2203
|
+
...this.options.messages !== void 0 ? {
|
|
2204
|
+
messages: this.options.messages
|
|
2205
|
+
} : {},
|
|
2206
|
+
// A tool result is the app's own data: a value redacted out of it is handed back to the
|
|
2207
|
+
// reader and to the next tool call like one the user typed. `restore: false` opts a rule out.
|
|
2208
|
+
restorableStages: RESTORABLE_STAGES
|
|
2209
|
+
});
|
|
2210
|
+
await this.report(ctx, result);
|
|
2211
|
+
return result;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Screens one tool definition (name, title, description, and every description in its input
|
|
2215
|
+
* schema) for hidden instructions. `allowed: false` means a `block`/`approve` rule fired — leave
|
|
2216
|
+
* the tool out of the catalog. Shaped to be handed straight to an MCP importer's screen hook.
|
|
2217
|
+
*/
|
|
2218
|
+
async screenTool(tool) {
|
|
2219
|
+
const ctx = {
|
|
2220
|
+
stage: "tool_description",
|
|
2221
|
+
tool: tool.name,
|
|
2222
|
+
...tool.server !== void 0 ? {
|
|
2223
|
+
server: tool.server
|
|
2224
|
+
} : {}
|
|
2225
|
+
};
|
|
2226
|
+
const result = await this.scan(ctx, [
|
|
2227
|
+
{
|
|
2228
|
+
text: toolText(tool)
|
|
2229
|
+
}
|
|
2230
|
+
]);
|
|
2231
|
+
if (isRefusal(result.action)) {
|
|
2232
|
+
const categories = [
|
|
2233
|
+
...new Set(result.hits.map((h) => h.category))
|
|
2234
|
+
].join(", ");
|
|
2235
|
+
return {
|
|
2236
|
+
allowed: false,
|
|
2237
|
+
reason: `${result.decisive?.message ?? "quarantined by guardrails"} (${categories})`
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
allowed: true
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Wraps a tool handler so its arguments are guarded where the loop cannot rewrite them: the
|
|
2246
|
+
* thread's placeholders are restored (the model only ever saw `[EMAIL_1]`, the tool needs the
|
|
2247
|
+
* address), then the `tool_args` rules run — `redact` rewrites what the tool receives, `block`
|
|
2248
|
+
* throws a {@link GuardrailBlockedError}, which the model reads as the tool's failure.
|
|
2249
|
+
*/
|
|
2250
|
+
wrapTool(toolName, handler) {
|
|
2251
|
+
return {
|
|
2252
|
+
execute: /* @__PURE__ */ __name(async (input, ctx) => {
|
|
2253
|
+
const vault = await this.vaults.load(ctx.threadId) ?? new Vault();
|
|
2254
|
+
const box = {
|
|
2255
|
+
value: cloneJson(input)
|
|
2256
|
+
};
|
|
2257
|
+
const slots = jsonSlots(box.value, "assistant", true, (text) => {
|
|
2258
|
+
box.value = text;
|
|
2259
|
+
});
|
|
2260
|
+
for (const slot of slots) slot.set(vault.restore(slot.segment.text));
|
|
2261
|
+
const guardCtx = {
|
|
2262
|
+
stage: "tool_args",
|
|
2263
|
+
tool: toolName,
|
|
2264
|
+
threadId: ctx.threadId,
|
|
2265
|
+
actor: ctx.actor,
|
|
2266
|
+
...ctx.agentName !== void 0 ? {
|
|
2267
|
+
agentName: ctx.agentName
|
|
2268
|
+
} : {}
|
|
2269
|
+
};
|
|
2270
|
+
const restoredSlots = jsonSlots(box.value, "assistant", true, (text) => {
|
|
2271
|
+
box.value = text;
|
|
2272
|
+
});
|
|
2273
|
+
const result = await this.scan(guardCtx, restoredSlots.map((s) => s.segment), vault);
|
|
2274
|
+
if (isRefusal(result.action)) {
|
|
2275
|
+
throw new GuardrailBlockedError("tool_args", result.decisive?.message ?? "blocked", result.decisive);
|
|
2276
|
+
}
|
|
2277
|
+
writeBack(restoredSlots, result);
|
|
2278
|
+
return handler.execute(box.value, ctx);
|
|
2279
|
+
}, "execute"),
|
|
2280
|
+
...handler.isEnabled !== void 0 ? {
|
|
2281
|
+
isEnabled: /* @__PURE__ */ __name(() => handler.isEnabled?.call(handler) ?? true, "isEnabled")
|
|
2282
|
+
} : {},
|
|
2283
|
+
...handler.canUse !== void 0 ? {
|
|
2284
|
+
canUse: /* @__PURE__ */ __name((actor) => handler.canUse?.call(handler, actor) ?? true, "canUse")
|
|
2285
|
+
} : {}
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
contextOf(stage, ctx, tool) {
|
|
2289
|
+
return {
|
|
2290
|
+
stage,
|
|
2291
|
+
threadId: ctx.threadId,
|
|
2292
|
+
actor: ctx.actor,
|
|
2293
|
+
step: ctx.step,
|
|
2294
|
+
...ctx.agentName !== void 0 ? {
|
|
2295
|
+
agentName: ctx.agentName
|
|
2296
|
+
} : {},
|
|
2297
|
+
...tool !== void 0 ? {
|
|
2298
|
+
tool
|
|
2299
|
+
} : {}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
async processInput(prompt, pctx) {
|
|
2303
|
+
if (pctx.step === 0) {
|
|
2304
|
+
this.reported.delete(pctx.threadId);
|
|
2305
|
+
}
|
|
2306
|
+
const vault = await this.vaults.load(pctx.threadId) ?? new Vault();
|
|
2307
|
+
const out = {
|
|
2308
|
+
system: prompt.system,
|
|
2309
|
+
messages: cloneJson(prompt.messages)
|
|
2310
|
+
};
|
|
2311
|
+
const from = turnStart(out.messages);
|
|
2312
|
+
const slots = [];
|
|
2313
|
+
if (out.system) {
|
|
2314
|
+
slots.push({
|
|
2315
|
+
segment: {
|
|
2316
|
+
text: out.system,
|
|
2317
|
+
source: "system",
|
|
2318
|
+
fresh: true
|
|
2319
|
+
},
|
|
2320
|
+
set: /* @__PURE__ */ __name((text) => {
|
|
2321
|
+
out.system = text;
|
|
2322
|
+
}, "set")
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
out.messages.forEach((message, i) => {
|
|
2326
|
+
const fresh = i >= from;
|
|
2327
|
+
if (message.content) {
|
|
2328
|
+
slots.push({
|
|
2329
|
+
segment: {
|
|
2330
|
+
text: message.content,
|
|
2331
|
+
source: sourceOf2(message.role),
|
|
2332
|
+
fresh
|
|
2333
|
+
},
|
|
2334
|
+
set: /* @__PURE__ */ __name((text) => {
|
|
2335
|
+
message.content = text;
|
|
2336
|
+
}, "set")
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
for (const call of message.toolCalls ?? []) {
|
|
2340
|
+
slots.push(...jsonSlots(call.input, "assistant", fresh, (text) => {
|
|
2341
|
+
call.input = text;
|
|
2342
|
+
}));
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
const request = await this.scan(this.contextOf("llm_request", pctx), slots.map((s) => s.segment), vault);
|
|
2346
|
+
if (isRefusal(request.action)) {
|
|
2347
|
+
await this.vaults.save(pctx.threadId, vault);
|
|
2348
|
+
throw new GuardrailBlockedError("llm_request", request.decisive?.message ?? "blocked", request.decisive);
|
|
2349
|
+
}
|
|
2350
|
+
writeBack(slots, request);
|
|
2351
|
+
for (const [i, message] of out.messages.entries()) {
|
|
2352
|
+
for (const result of message.toolResults ?? []) {
|
|
2353
|
+
await this.guardToolResult(result, i >= from, pctx, vault);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
await this.vaults.save(pctx.threadId, vault);
|
|
2357
|
+
return out;
|
|
2358
|
+
}
|
|
2359
|
+
async guardToolResult(result, fresh, pctx, vault) {
|
|
2360
|
+
const slots = jsonSlots(result.output, "tool", fresh, (text) => {
|
|
2361
|
+
result.output = text;
|
|
2362
|
+
});
|
|
2363
|
+
if (result.error) {
|
|
2364
|
+
slots.push({
|
|
2365
|
+
segment: {
|
|
2366
|
+
text: result.error,
|
|
2367
|
+
source: "tool",
|
|
2368
|
+
fresh
|
|
2369
|
+
},
|
|
2370
|
+
set: /* @__PURE__ */ __name((text) => {
|
|
2371
|
+
result.error = text;
|
|
2372
|
+
}, "set")
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
if (slots.length === 0) return;
|
|
2376
|
+
const scanned = await this.scan(this.contextOf("tool_result", pctx, result.name), slots.map((s) => s.segment), vault);
|
|
2377
|
+
if (isRefusal(scanned.action)) {
|
|
2378
|
+
result.output = scanned.decisive?.message ?? "withheld";
|
|
2379
|
+
if (result.error !== void 0) result.error = result.output;
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
writeBack(slots, scanned);
|
|
2383
|
+
}
|
|
2384
|
+
async processOutput(answer, pctx) {
|
|
2385
|
+
for (const call of answer.toolCalls) {
|
|
2386
|
+
const slots = jsonSlots(cloneJson(call.input), "assistant", true);
|
|
2387
|
+
if (slots.length === 0) continue;
|
|
2388
|
+
const args = await this.scan(this.contextOf("tool_args", pctx, call.name), slots.map((s) => s.segment));
|
|
2389
|
+
if (isRefusal(args.action)) {
|
|
2390
|
+
return {
|
|
2391
|
+
action: "reject",
|
|
2392
|
+
reason: args.decisive?.message ?? "blocked"
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (!answer.text) return {
|
|
2397
|
+
action: "pass"
|
|
2398
|
+
};
|
|
2399
|
+
const response = await this.scan(this.contextOf("llm_response", pctx), [
|
|
2400
|
+
{
|
|
2401
|
+
text: answer.text,
|
|
2402
|
+
source: "assistant"
|
|
2403
|
+
}
|
|
2404
|
+
], new Vault());
|
|
2405
|
+
if (isRefusal(response.action)) {
|
|
2406
|
+
return {
|
|
2407
|
+
action: "reject",
|
|
2408
|
+
reason: response.decisive?.message ?? "blocked"
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const vault = await this.vaults.load(pctx.threadId);
|
|
2412
|
+
const redacted = response.segments[0] ?? answer.text;
|
|
2413
|
+
const text = vault !== void 0 ? vault.restore(redacted) : redacted;
|
|
2414
|
+
return text === answer.text ? {
|
|
2415
|
+
action: "pass"
|
|
2416
|
+
} : {
|
|
2417
|
+
action: "replace",
|
|
2418
|
+
text
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
async report(ctx, result) {
|
|
2422
|
+
const onEvent = this.options.onEvent;
|
|
2423
|
+
if (onEvent === void 0 || result.hits.length === 0) return;
|
|
2424
|
+
const seen = ctx.threadId !== void 0 ? this.reported.get(ctx.threadId) ?? this.reported.set(ctx.threadId, /* @__PURE__ */ new Set()).get(ctx.threadId) : void 0;
|
|
2425
|
+
const fingerprint = this.options.fingerprint;
|
|
2426
|
+
const hits = [];
|
|
2427
|
+
for (const { values, ...hit } of result.hits) {
|
|
2428
|
+
if (!hit.fresh) continue;
|
|
2429
|
+
const key = `${ctx.stage}\0${ctx.tool ?? ""}\0${hit.ruleId}\0${hit.category}\0${values.join("")}`;
|
|
2430
|
+
if (seen?.has(key)) continue;
|
|
2431
|
+
seen?.add(key);
|
|
2432
|
+
hits.push({
|
|
2433
|
+
...hit,
|
|
2434
|
+
fingerprints: fingerprint !== void 0 ? values.map(fingerprint) : []
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
if (hits.length === 0) return;
|
|
2438
|
+
if (this.reported.size > 1e3) {
|
|
2439
|
+
const oldest = this.reported.keys().next().value;
|
|
2440
|
+
if (oldest !== void 0) this.reported.delete(oldest);
|
|
2441
|
+
}
|
|
2442
|
+
try {
|
|
2443
|
+
await onEvent({
|
|
2444
|
+
stage: result.stage,
|
|
2445
|
+
action: result.action,
|
|
2446
|
+
...result.decisive !== void 0 ? {
|
|
2447
|
+
decisive: result.decisive
|
|
2448
|
+
} : {},
|
|
2449
|
+
hits,
|
|
2450
|
+
redactions: result.redactions,
|
|
2451
|
+
latencyMs: result.latencyMs,
|
|
2452
|
+
context: ctx
|
|
2453
|
+
});
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
function createGuardrails(options = {}) {
|
|
2459
|
+
return new Guardrails(options);
|
|
2460
|
+
}
|
|
2461
|
+
__name(createGuardrails, "createGuardrails");
|
|
2462
|
+
function screenToolDefinition(tool, threshold = 0.5) {
|
|
2463
|
+
return scoreToolText(toolText(tool), threshold);
|
|
2464
|
+
}
|
|
2465
|
+
__name(screenToolDefinition, "screenToolDefinition");
|
|
2466
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2467
|
+
0 && (module.exports = {
|
|
2468
|
+
ACTION_SEVERITY,
|
|
2469
|
+
DEFAULT_APPROVAL_MESSAGE,
|
|
2470
|
+
DEFAULT_MESSAGES,
|
|
2471
|
+
GUARDRAIL_STAGES,
|
|
2472
|
+
GuardrailBlockedError,
|
|
2473
|
+
Guardrails,
|
|
2474
|
+
INJECTION_REPLACEMENT,
|
|
2475
|
+
INJECTION_SIGNALS,
|
|
2476
|
+
InMemoryVaultStore,
|
|
2477
|
+
PII_TYPES,
|
|
2478
|
+
SECRET_TYPES,
|
|
2479
|
+
StreamGuard,
|
|
2480
|
+
Vault,
|
|
2481
|
+
applyRedactions,
|
|
2482
|
+
cardBrand,
|
|
2483
|
+
cnpjValid,
|
|
2484
|
+
compileGuardPattern,
|
|
2485
|
+
cpfValid,
|
|
2486
|
+
createGuardrails,
|
|
2487
|
+
decodeTagChars,
|
|
2488
|
+
detectKeywords,
|
|
2489
|
+
detectPii,
|
|
2490
|
+
detectRegex,
|
|
2491
|
+
detectSecrets,
|
|
2492
|
+
entropy,
|
|
2493
|
+
fold,
|
|
2494
|
+
globToRegExp,
|
|
2495
|
+
ibanValid,
|
|
2496
|
+
isIPv4,
|
|
2497
|
+
jsonSlots,
|
|
2498
|
+
labelFor,
|
|
2499
|
+
luhnValid,
|
|
2500
|
+
matchesAny,
|
|
2501
|
+
orderRules,
|
|
2502
|
+
phoneValid,
|
|
2503
|
+
refuseResponse,
|
|
2504
|
+
requestSlots,
|
|
2505
|
+
resolveOverlaps,
|
|
2506
|
+
responseSlots,
|
|
2507
|
+
ruleApplies,
|
|
2508
|
+
runDetector,
|
|
2509
|
+
scan,
|
|
2510
|
+
scoreInjection,
|
|
2511
|
+
scoreToolText,
|
|
2512
|
+
screenToolDefinition,
|
|
2513
|
+
shorthandRules,
|
|
2514
|
+
spanLocator,
|
|
2515
|
+
ssnValid,
|
|
2516
|
+
toolResultSlots,
|
|
2517
|
+
toolText
|
|
2518
|
+
});
|
|
2519
|
+
//# sourceMappingURL=index.cjs.map
|