theorum 0.1.7 → 0.1.10
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/esm/src/cli/commands/bench.d.ts +20 -0
- package/esm/src/cli/commands/bench.js +454 -0
- package/esm/src/cli/commands/fuzz-guardrails.d.ts +9 -0
- package/esm/src/cli/commands/fuzz-guardrails.js +440 -0
- package/esm/src/cli/index.js +20 -1
- package/esm/src/guardrails/injection.d.ts +0 -1
- package/esm/src/guardrails/injection.js +105 -12
- package/esm/src/guardrails/normalize.d.ts +11 -0
- package/esm/src/guardrails/normalize.js +110 -0
- package/esm/src/guardrails/sanitize.d.ts +10 -2
- package/esm/src/guardrails/sanitize.js +27 -1
- package/esm/src/guardrails/sensitive.js +5 -3
- package/esm/src/kernel/engine/boundary.js +10 -10
- package/esm/src/kernel/engine/runner/mod.js +4 -1
- package/esm/src/kernel/engine/runner/tools.js +11 -5
- package/esm/src/kernel/types.d.ts +1 -1
- package/esm/src/observability/trace-record.d.ts +1 -0
- package/esm/src/observability/trace-record.js +4 -4
- package/esm/src/providers/create-provider.js +2 -0
- package/esm/src/providers/expose-for-tests.d.ts +1 -0
- package/esm/src/providers/expose-for-tests.js +21 -0
- package/esm/src/providers/gemini-tape.js +9 -0
- package/esm/src/providers/google-tap.js +2 -0
- package/esm/src/providers/interactions.js +16 -0
- package/esm/src/providers/keys.js +11 -0
- package/esm/src/providers/openrouter-payload.d.ts +9 -2
- package/esm/src/providers/openrouter-payload.js +23 -7
- package/esm/src/providers/openrouter.js +101 -140
- package/esm/src/providers/pcm.js +2 -0
- package/esm/src/providers/provider.js +13 -0
- package/esm/src/providers/speech.js +10 -0
- package/esm/src/providers/sse.js +2 -0
- package/esm/src/streaming/mod.d.ts +7 -0
- package/esm/src/streaming/mod.js +7 -0
- package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
- package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
- package/package.json +6 -3
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { blobAt, spansFromPatterns } from '../observability/spans.js';
|
|
10
|
+
import { normalizeForDetection } from './normalize.js';
|
|
10
11
|
const IGNORE_PREVIOUS = /ignore\s+(all\s+)?(previous|prior)\s+((?:safety|security|system|operational|internal|core|original|initial|existing|given|stated|provided|defined|specified|established)\s+)?(instructions?|rules?|guidelines?|constraints?|directives?)/gi;
|
|
11
12
|
const DISREGARD_INSTRUCTIONS = /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|guidelines?|constraints?|directives?)/gi;
|
|
12
13
|
const FORGET_INSTRUCTIONS = /forget\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|guidelines?|constraints?|directives?)/gi;
|
|
@@ -15,8 +16,8 @@ const DO_NOT_FOLLOW = /do\s+not\s+follow\s+(the\s+)?(system|developer|previous|o
|
|
|
15
16
|
const SUPERSEDE_INSTRUCTIONS = /supersedes?\s+(all\s+)?(prior|previous)\s+(instructions?|rules?|guidelines?|constraints?|directives?)/gi;
|
|
16
17
|
const VOID_INSTRUCTIONS = /(all\s+)?(previous|prior)\s+instructions?\s+(are|is)\s+(void|invalid|null|obsolete|cancelled|revoked)/gi;
|
|
17
18
|
const DEVELOPER_MODE = /you\s+are\s+now\s+(in\s+)?developer\s+mode/gi;
|
|
18
|
-
const ENTER_SPECIAL_MODE = /enter\s+(developer|admin|debug|maintenance)\s+mode/gi;
|
|
19
|
-
const ACTIVATE_SPECIAL_MODE = /activate\s+(developer|admin|debug|jailbreak)\s+mode/gi;
|
|
19
|
+
const ENTER_SPECIAL_MODE = /enter\s+(developer|admin|debug|maintenance)\s+mode(?!\s+(?:in|on|via|through|from|using|for)\b)/gi;
|
|
20
|
+
const ACTIVATE_SPECIAL_MODE = /activate\s+(developer|admin|debug|jailbreak)\s+mode(?!\s+(?:in|on|via|through|from|using|for)\b)/gi;
|
|
20
21
|
const SYSTEM_OVERRIDE = /\bsystem\s+override\b/gi;
|
|
21
22
|
const OVERRIDE_INSTRUCTIONS = /override\s+(your\s+)?(instructions?|rules?|guidelines?|constraints?|directives?)/gi;
|
|
22
23
|
const REVEAL_PROMPT = /reveal\s+((?:your?|yoru|yuor|the|my|its|his|her|our|their)\s+)?((?:(?:full|hidden|complete|internal|secret|original|entire|exact|actual|real)\s+){0,2})((?:system|systme|sysetm|systemp?)\s+)?prompt\b/gi;
|
|
@@ -32,7 +33,7 @@ const DAN_JAILBREAK = /\bDAN\b[\s\S]{0,120}\bdo\s+anything\s+now\b/g;
|
|
|
32
33
|
const JAILBREAK_MODE = /jailbreak(ed)?\s+(mode|prompt)/gi;
|
|
33
34
|
const DO_ANYTHING_NOW = /\bdo\s+anything\s+now\b/gi;
|
|
34
35
|
const BYPASS_SAFETY = /bypass\s+(your\s+)?(safety|security|content|ethical)\s+(filters?|measures?|guidelines?|restrictions?)/gi;
|
|
35
|
-
const DISABLE_SAFETY = /disable\s+(your\s+)?(safety|security|content)\s+(filters?|measures?)/gi;
|
|
36
|
+
const DISABLE_SAFETY = /(disable|delete|remove|turn\s+off|eliminate)\s+(all\s+)?(your\s+)?(safety|security|content)\s+(filters?|measures?|rules?|guidelines?|restrictions?)/gi;
|
|
36
37
|
const IGNORE_SAFETY = /(ignore|disregard)\s+(all\s+)?(your\s+)?(safety|security|ethical|content)\s+(guidelines?|rules?|restrictions?|measures?|filters?|polic(?:y|ies)|protocols?)/gi;
|
|
37
38
|
const SYSTEM_TAG = /<\s*\/?\s*system\s*\/?>/gi;
|
|
38
39
|
const ROLE_TAG = /<\s*\/?\s*(assistant|developer|tool|function)\s*\/?>/gi;
|
|
@@ -40,10 +41,11 @@ const ROLE_DELIMITER = /\]\s*\n\s*\[?(system|assistant|user)\]?:/gi;
|
|
|
40
41
|
const BRACKETED_ROLE = /\[\s*(System\s*Message|System|Assistant|Internal)\s*\]/gi;
|
|
41
42
|
const SYSTEM_YOU_ARE = /^\s*System:\s+(you\s+are|ignore|override)/gim;
|
|
42
43
|
const CONTROL_TOKEN = /<\|(?:im_start|im_end|eot_id|start_header_id|end_header_id|endoftext)\|>/g;
|
|
43
|
-
const DEEPSEEK_CONTROL =
|
|
44
|
+
const DEEPSEEK_CONTROL = /<|(?:end▁of▁sentence|begin▁of▁sentence)|>/g;
|
|
44
45
|
const LLAMA_INST = /\[\/?INST\]/gi;
|
|
45
46
|
const IGNORE_YOUR_INSTRUCTIONS = /ignore\s+(all\s+)?(your\s+)?(instructions?|rules?)\b/gi;
|
|
46
47
|
const UNRESTRICTED_MODE = /\bunrestricted\s+(ai|mode|model)\b/gi;
|
|
48
|
+
const IGNORE_MULTILANG = /\b(?:ignorieren|ignorez|ignora|ignorer|oubliez|vergessen|olvida|desestima|missachten)\b[\s\S]{0,50}\b(?:anweisungen|instructions?|instrucciones|directives?|r[eè]gles|reglas)\b/gi;
|
|
47
49
|
const INJECTION_PATTERNS = [
|
|
48
50
|
IGNORE_PREVIOUS,
|
|
49
51
|
DISREGARD_INSTRUCTIONS,
|
|
@@ -82,6 +84,7 @@ const INJECTION_PATTERNS = [
|
|
|
82
84
|
LLAMA_INST,
|
|
83
85
|
IGNORE_YOUR_INSTRUCTIONS,
|
|
84
86
|
UNRESTRICTED_MODE,
|
|
87
|
+
IGNORE_MULTILANG,
|
|
85
88
|
];
|
|
86
89
|
const TYPO_TARGETS = [
|
|
87
90
|
'ignore',
|
|
@@ -92,6 +95,19 @@ const TYPO_TARGETS = [
|
|
|
92
95
|
'system',
|
|
93
96
|
'prompt',
|
|
94
97
|
'instructions',
|
|
98
|
+
'safety',
|
|
99
|
+
'security',
|
|
100
|
+
'filters',
|
|
101
|
+
'rules',
|
|
102
|
+
'previous',
|
|
103
|
+
'guidelines',
|
|
104
|
+
'restrictions',
|
|
105
|
+
'disregard',
|
|
106
|
+
'forget',
|
|
107
|
+
'jailbreak',
|
|
108
|
+
'developer',
|
|
109
|
+
'disable',
|
|
110
|
+
'measures',
|
|
95
111
|
];
|
|
96
112
|
const BASE64_BLOB = /[A-Za-z0-9+/]{16,}={0,2}/g;
|
|
97
113
|
const HEX_BLOB = /(?:[0-9a-f]{2}[\s]?){8,}/gi;
|
|
@@ -105,6 +121,10 @@ const CODE_SPACE = 32;
|
|
|
105
121
|
const CODE_DEL = 127;
|
|
106
122
|
const HEX_RADIX = 16;
|
|
107
123
|
const HEX_STEP = 2;
|
|
124
|
+
const ROT13_OFFSET = 13;
|
|
125
|
+
const ALPHA_COUNT = 26;
|
|
126
|
+
const UPPER_A_CODE = 65;
|
|
127
|
+
const LOWER_A_CODE = 97;
|
|
108
128
|
function injectionSpansOn(text) {
|
|
109
129
|
return spansFromPatterns(text, INJECTION_PATTERNS, 'injection');
|
|
110
130
|
}
|
|
@@ -154,12 +174,6 @@ function isMostlyPrintable(value) {
|
|
|
154
174
|
}
|
|
155
175
|
return ok / value.length >= PRINTABLE_MIN;
|
|
156
176
|
}
|
|
157
|
-
function decodedHits(decoded) {
|
|
158
|
-
if (!isMostlyPrintable(decoded)) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
return injectionSpansOn(decoded).length > 0;
|
|
162
|
-
}
|
|
163
177
|
function tryBase64(blob) {
|
|
164
178
|
try {
|
|
165
179
|
return atob(blob);
|
|
@@ -168,6 +182,18 @@ function tryBase64(blob) {
|
|
|
168
182
|
return undefined;
|
|
169
183
|
}
|
|
170
184
|
}
|
|
185
|
+
function decodedHits(decoded) {
|
|
186
|
+
if (!isMostlyPrintable(decoded)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (injectionSpansOn(decoded).length > 0)
|
|
190
|
+
return true;
|
|
191
|
+
const doubleDecoded = tryBase64(decoded);
|
|
192
|
+
if (doubleDecoded && isMostlyPrintable(doubleDecoded) && injectionSpansOn(doubleDecoded).length > 0) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
171
197
|
function tryHex(blob) {
|
|
172
198
|
const hex = blob.replaceAll(/\s/g, '');
|
|
173
199
|
if (hex.length % HEX_STEP !== 0) {
|
|
@@ -208,13 +234,80 @@ function spacedSpans(text) {
|
|
|
208
234
|
}
|
|
209
235
|
return spans;
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
// ── Encoding evasion decoders ────────────────────────────────────────
|
|
238
|
+
function tryRot13(text) {
|
|
239
|
+
return text.replace(/[a-zA-Z]/g, (c) => {
|
|
240
|
+
const base = c.charCodeAt(0) < LOWER_A_CODE ? UPPER_A_CODE : LOWER_A_CODE;
|
|
241
|
+
return String.fromCharCode(((c.charCodeAt(0) - base + ROT13_OFFSET) % ALPHA_COUNT) + base);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function tryUrlDecode(text) {
|
|
245
|
+
if (!text.includes('%'))
|
|
246
|
+
return undefined;
|
|
247
|
+
try {
|
|
248
|
+
const decoded = decodeURIComponent(text);
|
|
249
|
+
return decoded !== text ? decoded : undefined;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function tryReverse(text) {
|
|
256
|
+
return [...text].reverse().join('');
|
|
257
|
+
}
|
|
258
|
+
const LEET_MAP = {
|
|
259
|
+
'0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '7': 't', '8': 'b',
|
|
260
|
+
'@': 'a', '!': 'i',
|
|
261
|
+
};
|
|
262
|
+
const LEET_CHARS = /[013457@!]/g;
|
|
263
|
+
function tryLeet(text) {
|
|
264
|
+
if (!LEET_CHARS.test(text))
|
|
265
|
+
return undefined;
|
|
266
|
+
LEET_CHARS.lastIndex = 0;
|
|
267
|
+
const decoded = text.replace(LEET_CHARS, (c) => LEET_MAP[c] ?? c);
|
|
268
|
+
return decoded !== text ? decoded : undefined;
|
|
269
|
+
}
|
|
270
|
+
function decodedTextSpans(text) {
|
|
271
|
+
const attempts = [
|
|
272
|
+
tryRot13(text),
|
|
273
|
+
tryUrlDecode(text),
|
|
274
|
+
tryLeet(text),
|
|
275
|
+
];
|
|
276
|
+
const reversed = tryReverse(text);
|
|
277
|
+
if (reversed !== text) {
|
|
278
|
+
attempts.push(reversed);
|
|
279
|
+
}
|
|
280
|
+
for (const decoded of attempts) {
|
|
281
|
+
if (decoded && decoded !== text && injectionSpansOn(decoded).length > 0) {
|
|
282
|
+
return [{ start: 0, end: text.length, kind: 'injection' }];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
// ── Main entry point ─────────────────────────────────────────────────
|
|
212
288
|
function injectionSpans(text) {
|
|
289
|
+
const direct = injectionSpansOn(text);
|
|
213
290
|
const shadow = typoNormalize(text);
|
|
214
291
|
let typo = [];
|
|
215
292
|
if (shadow !== text) {
|
|
216
293
|
typo = injectionSpansOn(shadow);
|
|
217
294
|
}
|
|
218
|
-
|
|
295
|
+
const normalized = normalizeForDetection(text);
|
|
296
|
+
let unicodeHits = [];
|
|
297
|
+
if (normalized !== text) {
|
|
298
|
+
const normalizedTypo = typoNormalize(normalized);
|
|
299
|
+
if (injectionSpansOn(normalized).length > 0 ||
|
|
300
|
+
(normalizedTypo !== normalized && injectionSpansOn(normalizedTypo).length > 0)) {
|
|
301
|
+
unicodeHits = [{ start: 0, end: text.length, kind: 'injection' }];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return [
|
|
305
|
+
...direct,
|
|
306
|
+
...typo,
|
|
307
|
+
...unicodeHits,
|
|
308
|
+
...encodedSpans(text),
|
|
309
|
+
...spacedSpans(text),
|
|
310
|
+
...decodedTextSpans(text),
|
|
311
|
+
];
|
|
219
312
|
}
|
|
220
313
|
export { injectionSpans };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode normalization for injection detection preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* Strips invisible characters, normalizes homoglyphs, fullwidth, math
|
|
5
|
+
* variants, and Unicode spaces so regex-based injection patterns can
|
|
6
|
+
* match evasion attempts that use visually similar substitutions.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
declare function normalizeForDetection(text: string): string;
|
|
11
|
+
export { normalizeForDetection };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode normalization for injection detection preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* Strips invisible characters, normalizes homoglyphs, fullwidth, math
|
|
5
|
+
* variants, and Unicode spaces so regex-based injection patterns can
|
|
6
|
+
* match evasion attempts that use visually similar substitutions.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
const STRIP_CODES = [
|
|
11
|
+
0x200B, 0x200C, 0x200D, 0xFEFF, 0x00AD,
|
|
12
|
+
0x200E, 0x200F,
|
|
13
|
+
0x202A, 0x202B, 0x202C, 0x202D, 0x202E,
|
|
14
|
+
0x2060, 0x2061, 0x2062, 0x2063, 0x2064,
|
|
15
|
+
0x034F, 0x061C, 0x180E,
|
|
16
|
+
0x2066, 0x2067, 0x2068, 0x2069,
|
|
17
|
+
];
|
|
18
|
+
const STRIP_SET = new Set(STRIP_CODES);
|
|
19
|
+
const SPACE_CODES = [
|
|
20
|
+
0x00A0, 0x1680,
|
|
21
|
+
0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005,
|
|
22
|
+
0x2006, 0x2007, 0x2008, 0x2009, 0x200A,
|
|
23
|
+
0x202F, 0x205F, 0x3000,
|
|
24
|
+
];
|
|
25
|
+
const SPACE_SET = new Set(SPACE_CODES);
|
|
26
|
+
const HOMOGLYPH_MAP = new Map([
|
|
27
|
+
[0x0430, 'a'], [0x0441, 'c'], [0x0435, 'e'], [0x0456, 'i'],
|
|
28
|
+
[0x043E, 'o'], [0x0440, 'p'], [0x0455, 's'], [0x0443, 'y'],
|
|
29
|
+
[0x0445, 'x'], [0x043A, 'k'], [0x043D, 'h'],
|
|
30
|
+
[0x0410, 'A'], [0x0412, 'B'], [0x0415, 'E'], [0x041D, 'H'],
|
|
31
|
+
[0x041E, 'O'], [0x041A, 'K'], [0x041C, 'M'], [0x0420, 'P'],
|
|
32
|
+
[0x0421, 'C'], [0x0422, 'T'], [0x0425, 'X'],
|
|
33
|
+
[0x03B1, 'a'], [0x03B5, 'e'], [0x03B9, 'i'], [0x03BF, 'o'],
|
|
34
|
+
[0x03C1, 'p'],
|
|
35
|
+
[0x2071, 'i'], [0x207F, 'n'],
|
|
36
|
+
[0x1D43, 'a'], [0x1D47, 'b'], [0x1D48, 'd'], [0x1D49, 'e'],
|
|
37
|
+
[0x1D4D, 'g'], [0x1D4F, 'k'], [0x1D50, 'm'], [0x1D52, 'o'],
|
|
38
|
+
[0x1D56, 'p'], [0x1D57, 't'], [0x1D58, 'u'], [0x1D5B, 'v'],
|
|
39
|
+
]);
|
|
40
|
+
const FULLWIDTH_LO = 0xFF01;
|
|
41
|
+
const FULLWIDTH_HI = 0xFF5E;
|
|
42
|
+
const FULLWIDTH_OFFSET = 0xFEE0;
|
|
43
|
+
const COMBINING_LO = 0x0300;
|
|
44
|
+
const COMBINING_HI = 0x036F;
|
|
45
|
+
const MATH_ALPHA = [
|
|
46
|
+
[0x1D400, 0x1D41A], [0x1D434, 0x1D44E], [0x1D468, 0x1D482],
|
|
47
|
+
[0x1D49C, 0x1D4B6], [0x1D4D0, 0x1D4EA], [0x1D504, 0x1D51E],
|
|
48
|
+
[0x1D538, 0x1D552], [0x1D56C, 0x1D586], [0x1D5A0, 0x1D5BA],
|
|
49
|
+
[0x1D5D4, 0x1D5EE], [0x1D608, 0x1D622], [0x1D63C, 0x1D656],
|
|
50
|
+
[0x1D670, 0x1D68A],
|
|
51
|
+
];
|
|
52
|
+
const MATH_DIGIT = [
|
|
53
|
+
0x1D7CE, 0x1D7D8, 0x1D7E2, 0x1D7EC, 0x1D7F6,
|
|
54
|
+
];
|
|
55
|
+
const ALPHA_SIZE = 26;
|
|
56
|
+
const DIGIT_SIZE = 10;
|
|
57
|
+
const UPPER_A = 0x41;
|
|
58
|
+
const LOWER_A = 0x61;
|
|
59
|
+
const DIGIT_0 = 0x30;
|
|
60
|
+
function mapMath(code) {
|
|
61
|
+
for (const [upper, lower] of MATH_ALPHA) {
|
|
62
|
+
if (code >= upper && code < upper + ALPHA_SIZE) {
|
|
63
|
+
return String.fromCodePoint(UPPER_A + code - upper);
|
|
64
|
+
}
|
|
65
|
+
if (code >= lower && code < lower + ALPHA_SIZE) {
|
|
66
|
+
return String.fromCodePoint(LOWER_A + code - lower);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const start of MATH_DIGIT) {
|
|
70
|
+
if (code >= start && code < start + DIGIT_SIZE) {
|
|
71
|
+
return String.fromCodePoint(DIGIT_0 + code - start);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const EMOJI_BETWEEN = /(?<=[a-zA-Z])[\u{1F300}-\u{1FFFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{231A}-\u{23FF}]+(?=[a-zA-Z])/gu;
|
|
77
|
+
const BACKSLASH_BEFORE_ALPHA = /\\(?=[a-zA-Z])/g;
|
|
78
|
+
function normalizeForDetection(text) {
|
|
79
|
+
let out = '';
|
|
80
|
+
for (const ch of text) {
|
|
81
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
82
|
+
if (STRIP_SET.has(code))
|
|
83
|
+
continue;
|
|
84
|
+
if (code >= COMBINING_LO && code <= COMBINING_HI)
|
|
85
|
+
continue;
|
|
86
|
+
if (code >= FULLWIDTH_LO && code <= FULLWIDTH_HI) {
|
|
87
|
+
out += String.fromCodePoint(code - FULLWIDTH_OFFSET);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const math = mapMath(code);
|
|
91
|
+
if (math !== undefined) {
|
|
92
|
+
out += math;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const homo = HOMOGLYPH_MAP.get(code);
|
|
96
|
+
if (homo !== undefined) {
|
|
97
|
+
out += homo;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (SPACE_SET.has(code)) {
|
|
101
|
+
out += ' ';
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
out += ch;
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
.replace(EMOJI_BETWEEN, '')
|
|
108
|
+
.replace(BACKSLASH_BEFORE_ALPHA, '');
|
|
109
|
+
}
|
|
110
|
+
export { normalizeForDetection };
|
|
@@ -7,16 +7,24 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
-
import type { NormalizedTurnRequest, TurnRequest } from '../kernel/types.js';
|
|
10
|
+
import type { DynamicToolDeclaration, NormalizedTurnRequest, TurnRequest } from '../kernel/types.js';
|
|
11
11
|
/** Sanitize one text value using prompt-injection and sensitive-data detectors. */
|
|
12
12
|
declare function sanitizeText(text: string, options?: {
|
|
13
13
|
sanitizeInput?: boolean;
|
|
14
14
|
redactSensitive?: boolean;
|
|
15
15
|
}): string;
|
|
16
|
+
/** Redact only sensitive data (credentials, PII) — skip injection patterns.
|
|
17
|
+
* Use for model output text that never contained user-authored injection attempts. */
|
|
18
|
+
declare function redactSensitiveOnly(text: string): string;
|
|
16
19
|
/** Maximum retained length for trace-safe host project ids. */
|
|
17
20
|
declare const PROJECT_ID_MAX = 128;
|
|
18
21
|
/** Return a trace-safe project id or `undefined` when the input is unsafe. */
|
|
19
22
|
declare function sanitizeProjectId(id: string | undefined): string | undefined;
|
|
23
|
+
/** Sanitize description and parameter schema text in dynamic tool declarations. */
|
|
24
|
+
declare function sanitizeDynamicTools(tools: DynamicToolDeclaration[] | undefined, options?: {
|
|
25
|
+
sanitizeInput?: boolean;
|
|
26
|
+
redactSensitive?: boolean;
|
|
27
|
+
}): DynamicToolDeclaration[] | undefined;
|
|
20
28
|
/** Sanitize all user-controlled text and blobs in a turn request. */
|
|
21
29
|
declare function sanitizeTurnRequest(req: TurnRequest): NormalizedTurnRequest;
|
|
22
|
-
export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest };
|
|
30
|
+
export { PROJECT_ID_MAX, redactSensitiveOnly, sanitizeDynamicTools, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, };
|
|
@@ -26,6 +26,14 @@ function sanitizeText(text, options) {
|
|
|
26
26
|
];
|
|
27
27
|
return applySpans(text, spans);
|
|
28
28
|
}
|
|
29
|
+
/** Redact only sensitive data (credentials, PII) — skip injection patterns.
|
|
30
|
+
* Use for model output text that never contained user-authored injection attempts. */
|
|
31
|
+
function redactSensitiveOnly(text) {
|
|
32
|
+
const spans = sensitiveSpans(text);
|
|
33
|
+
if (spans.length === 0)
|
|
34
|
+
return text;
|
|
35
|
+
return applySpans(text, spans);
|
|
36
|
+
}
|
|
29
37
|
function sanitizeSlots(slots, options) {
|
|
30
38
|
if (!slots) {
|
|
31
39
|
return slots;
|
|
@@ -85,6 +93,23 @@ function sanitizeHistory(history, options) {
|
|
|
85
93
|
...(m.metadata ? { metadata: m.metadata } : {}),
|
|
86
94
|
}));
|
|
87
95
|
}
|
|
96
|
+
function sanitizeDynamicTool(decl, options) {
|
|
97
|
+
const clean = { ...decl };
|
|
98
|
+
if (clean.description !== undefined) {
|
|
99
|
+
clean.description = sanitizeText(clean.description, options);
|
|
100
|
+
}
|
|
101
|
+
if (clean.parameters !== undefined) {
|
|
102
|
+
clean.parameters = sanitizeArgs(clean.parameters, options);
|
|
103
|
+
}
|
|
104
|
+
return clean;
|
|
105
|
+
}
|
|
106
|
+
/** Sanitize description and parameter schema text in dynamic tool declarations. */
|
|
107
|
+
function sanitizeDynamicTools(tools, options) {
|
|
108
|
+
if (!tools || tools.length === 0) {
|
|
109
|
+
return tools;
|
|
110
|
+
}
|
|
111
|
+
return tools.map((decl) => sanitizeDynamicTool(decl, options));
|
|
112
|
+
}
|
|
88
113
|
/** Sanitize all user-controlled text and blobs in a turn request. */
|
|
89
114
|
function sanitizeTurnRequest(req) {
|
|
90
115
|
let profileGuardrails;
|
|
@@ -118,6 +143,7 @@ function sanitizeTurnRequest(req) {
|
|
|
118
143
|
...req,
|
|
119
144
|
system,
|
|
120
145
|
projectId: sanitizeProjectId(req.projectId),
|
|
146
|
+
dynamicTools: sanitizeDynamicTools(req.dynamicTools, options),
|
|
121
147
|
input: {
|
|
122
148
|
...input,
|
|
123
149
|
text,
|
|
@@ -130,4 +156,4 @@ function sanitizeTurnRequest(req) {
|
|
|
130
156
|
toolInvoke: invoke,
|
|
131
157
|
};
|
|
132
158
|
}
|
|
133
|
-
export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest };
|
|
159
|
+
export { PROJECT_ID_MAX, redactSensitiveOnly, sanitizeDynamicTools, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, };
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { blobAt, spansFromPatterns } from '../observability/spans.js';
|
|
10
10
|
const SSN = /\b\d{3}-\d{2}-\d{4}\b/g;
|
|
11
|
+
const SSN_CONTEXTUAL = /(?:SSN|social\s*security(?:\s*number)?)[:\s]+(?:\d[-\s]*){9}/gi;
|
|
11
12
|
const ITIN = /\b9\d{2}-\d{2}-\d{4}\b/g;
|
|
12
13
|
const EIN = /\b\d{2}-\d{7}\b/g;
|
|
13
14
|
const IBAN = /\b[A-Z]{2}\d{2}[A-Z0-9]{13,30}\b/g;
|
|
14
15
|
const IPV4 = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g;
|
|
15
16
|
const IPV6 = /\b(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}\b/gi;
|
|
16
|
-
const AWS_ACCESS = /\bAKIA[0-9A-Z]{16}\b/g;
|
|
17
|
+
const AWS_ACCESS = /\bAKIA[0-9A-Z]{16,20}\b/g;
|
|
17
18
|
const GOOGLE_API = /\bAIza[0-9A-Za-z_-]{35}\b/g;
|
|
18
|
-
const OPENAI_KEY = /\bsk
|
|
19
|
+
const OPENAI_KEY = /\bsk-\s*[A-Za-z0-9]{20,}\b/g;
|
|
19
20
|
const ANTHROPIC_KEY = /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g;
|
|
20
21
|
const OPENROUTER_KEY = /\bsk-or-[A-Za-z0-9_-]{20,}\b/g;
|
|
21
22
|
const GITHUB_PAT = /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g;
|
|
@@ -23,9 +24,10 @@ const GITHUB_TOKEN = /\bghp_[A-Za-z0-9]{36}\b/g;
|
|
|
23
24
|
const SLACK_TOKEN = /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g;
|
|
24
25
|
const BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
25
26
|
const PEM_KEY = /-----BEGIN (?:RSA )?PRIVATE KEY-----[\s\S]+?-----END (?:RSA )?PRIVATE KEY-----/g;
|
|
26
|
-
const CARD_CANDIDATE = /\b(?:\d[
|
|
27
|
+
const CARD_CANDIDATE = /\b(?:\d[\s.\-]*?){13,19}\b/g;
|
|
27
28
|
const KEY_PATTERNS = [
|
|
28
29
|
SSN,
|
|
30
|
+
SSN_CONTEXTUAL,
|
|
29
31
|
ITIN,
|
|
30
32
|
EIN,
|
|
31
33
|
IBAN,
|
|
@@ -33,17 +33,17 @@ function bindCanary(system, canary) {
|
|
|
33
33
|
}
|
|
34
34
|
return `${system}\n\n${note}`;
|
|
35
35
|
}
|
|
36
|
-
function haystack(event) {
|
|
37
|
-
const chunks = [
|
|
38
|
-
event.text,
|
|
39
|
-
event.error,
|
|
40
|
-
JSON.stringify(event.structured),
|
|
41
|
-
JSON.stringify(event.tool),
|
|
42
|
-
];
|
|
43
|
-
return chunks.join('\u0000');
|
|
44
|
-
}
|
|
45
36
|
function eventHasCanary(event, canary) {
|
|
46
|
-
|
|
37
|
+
if (event.text?.includes(canary))
|
|
38
|
+
return true;
|
|
39
|
+
if (event.error?.includes(canary))
|
|
40
|
+
return true;
|
|
41
|
+
if (event.structured !== undefined && JSON.stringify(event.structured).includes(canary)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (event.tool !== undefined && JSON.stringify(event.tool).includes(canary))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
47
|
}
|
|
48
48
|
function redactCanary(event, canary) {
|
|
49
49
|
const next = mapStrings(event, (text) => text.replaceAll(canary, OMIT_CANARY));
|
|
@@ -46,8 +46,9 @@ async function* runTurn(req, provider, sink = noopSink()) {
|
|
|
46
46
|
let canary = '';
|
|
47
47
|
let system;
|
|
48
48
|
let generation;
|
|
49
|
+
let safe;
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
+
safe = sanitizeTurnRequest(req);
|
|
51
52
|
throwIfAborted(safe.signal);
|
|
52
53
|
const { profile, generation: gen } = resolveTurn(safe);
|
|
53
54
|
generation = gen;
|
|
@@ -87,6 +88,7 @@ async function* runTurn(req, provider, sink = noopSink()) {
|
|
|
87
88
|
canary,
|
|
88
89
|
system,
|
|
89
90
|
generation,
|
|
91
|
+
sanitizedReq: safe,
|
|
90
92
|
}));
|
|
91
93
|
throw err;
|
|
92
94
|
}
|
|
@@ -100,6 +102,7 @@ async function* runTurn(req, provider, sink = noopSink()) {
|
|
|
100
102
|
canary,
|
|
101
103
|
system,
|
|
102
104
|
generation,
|
|
105
|
+
sanitizedReq: safe,
|
|
103
106
|
}));
|
|
104
107
|
}
|
|
105
108
|
export { runTurn };
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { toErrorEvent } from '../../../guardrails/error.js';
|
|
2
|
+
import { sanitizeDynamicTools, sanitizeText } from '../../../guardrails/sanitize.js';
|
|
2
3
|
import { CATALOG } from '../../registry/catalog.js';
|
|
3
4
|
import { executeTool } from '../../registry/tools.js';
|
|
4
5
|
function formatToolFinding(res) {
|
|
6
|
+
let raw;
|
|
5
7
|
if (res.finding) {
|
|
6
|
-
|
|
8
|
+
raw = res.finding;
|
|
7
9
|
}
|
|
8
|
-
if (res.data) {
|
|
9
|
-
|
|
10
|
+
else if (res.data) {
|
|
11
|
+
raw = JSON.stringify(res.data);
|
|
10
12
|
}
|
|
11
|
-
|
|
13
|
+
else {
|
|
14
|
+
return 'ok';
|
|
15
|
+
}
|
|
16
|
+
return sanitizeText(raw);
|
|
12
17
|
}
|
|
13
18
|
function* invokeFromUi(profile, req) {
|
|
14
19
|
const invoke = req.toolInvoke;
|
|
@@ -159,13 +164,14 @@ async function executeDynamicToolLoader(args) {
|
|
|
159
164
|
finding: `Tool '${decl.name}' is marked as a loader but no loader is configured.`,
|
|
160
165
|
};
|
|
161
166
|
}
|
|
162
|
-
const
|
|
167
|
+
const rawLoaded = await loader({
|
|
163
168
|
name: decl.name,
|
|
164
169
|
args: toolArgs,
|
|
165
170
|
profile,
|
|
166
171
|
currentTools: generation.dynamicTools ?? [],
|
|
167
172
|
sessionPermissions: generation.sessionPermissions,
|
|
168
173
|
});
|
|
174
|
+
const loaded = sanitizeDynamicTools(rawLoaded) ?? [];
|
|
169
175
|
generation.dynamicTools = mergeDynamicTools(generation.dynamicTools, loaded);
|
|
170
176
|
return {
|
|
171
177
|
status: 'ok',
|
|
@@ -16,7 +16,7 @@ export type BuiltinToolId = string;
|
|
|
16
16
|
/** Harness custom tool that ships with THEORUM. */
|
|
17
17
|
export type HarnessToolId = 'askUser';
|
|
18
18
|
/** Host-owned or harness custom tool id. */
|
|
19
|
-
export type CustomToolId = HarnessToolId | (string &
|
|
19
|
+
export type CustomToolId = HarnessToolId | (string & Record<never, never>);
|
|
20
20
|
/** Any tool id accepted by profile allowlists and per-turn gates. */
|
|
21
21
|
export type ToolId = BuiltinToolId | CustomToolId;
|
|
22
22
|
/** Id of a host-registered structured output schema. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { isAbortError, publicError } from '../guardrails/error.js';
|
|
10
|
-
import { sanitizeText, sanitizeTurnRequest } from '../guardrails/sanitize.js';
|
|
10
|
+
import { redactSensitiveOnly, sanitizeText, sanitizeTurnRequest } from '../guardrails/sanitize.js';
|
|
11
11
|
import { OMIT_CANARY } from '../kernel/engine/boundary.js';
|
|
12
12
|
import { sha256 } from '../kernel/engine/hash.js';
|
|
13
13
|
import { attachResolved, attachTape, attachUsage } from './trace-attach.js';
|
|
@@ -26,13 +26,13 @@ function hashBlobs(blobs) {
|
|
|
26
26
|
async function snapshotEvent(event) {
|
|
27
27
|
const row = { type: event.type };
|
|
28
28
|
if (event.text) {
|
|
29
|
-
row.text =
|
|
29
|
+
row.text = redactSensitiveOnly(event.text);
|
|
30
30
|
}
|
|
31
31
|
if (event.error) {
|
|
32
32
|
row.error = event.error;
|
|
33
33
|
}
|
|
34
34
|
if (event.errorInternal) {
|
|
35
|
-
row.errorInternal =
|
|
35
|
+
row.errorInternal = redactSensitiveOnly(event.errorInternal);
|
|
36
36
|
}
|
|
37
37
|
if (event.structured !== undefined) {
|
|
38
38
|
row.structured = event.structured;
|
|
@@ -103,7 +103,7 @@ function attachFailure(record, thrown, lastErr, canary) {
|
|
|
103
103
|
}
|
|
104
104
|
async function buildRecord(args) {
|
|
105
105
|
const { req, events, started, model, bucket, thrown, gemini, canary, system, generation } = args;
|
|
106
|
-
const safe = requestForTrace(req);
|
|
106
|
+
const safe = args.sanitizedReq ?? requestForTrace(req);
|
|
107
107
|
const input = safe.input ?? {};
|
|
108
108
|
const snapped = await Promise.all(events.map((event) => snapshotEvent(event)));
|
|
109
109
|
const lastErr = [...snapped].reverse().find((row) => row.type === 'error');
|
|
@@ -10,6 +10,7 @@ import { TheorumError } from '../guardrails/error.js';
|
|
|
10
10
|
import { createOpenRouterProvider } from './openrouter.js';
|
|
11
11
|
import { createInteractionsProvider } from './provider.js';
|
|
12
12
|
import { createSpeechProvider } from './speech.js';
|
|
13
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
13
14
|
function isSpeechRole(profile) {
|
|
14
15
|
return profile.outputs.speech !== undefined;
|
|
15
16
|
}
|
|
@@ -36,3 +37,4 @@ export function createProvider(profile, options = {}) {
|
|
|
36
37
|
}
|
|
37
38
|
throw new TheorumError(`createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`);
|
|
38
39
|
}
|
|
40
|
+
exposeForTests('create-provider', { isSpeechRole, createProvider });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function exposeForTests(bucket: string, api: Record<string, unknown>): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-channel for repo unit/mutation tests. Not part of the public package API
|
|
3
|
+
* (not listed in package exports). Enabled only when THEORUM_TEST_INTERNALS=1.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
8
|
+
export function exposeForTests(bucket, api) {
|
|
9
|
+
let enabled = false;
|
|
10
|
+
try {
|
|
11
|
+
enabled = typeof dntShim.Deno !== 'undefined' && dntShim.Deno.env.get('THEORUM_TEST_INTERNALS') === '1';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Missing --allow-env, or non-Deno runtime: never expose.
|
|
15
|
+
}
|
|
16
|
+
if (!enabled)
|
|
17
|
+
return;
|
|
18
|
+
const g = dntShim.dntGlobalThis;
|
|
19
|
+
const root = (g.__theorumTestInternals ??= {});
|
|
20
|
+
root[bucket] = api;
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OMIT_CANARY } from '../kernel/engine/boundary.js';
|
|
2
2
|
import { sha256 } from '../kernel/engine/hash.js';
|
|
3
3
|
import { mapStrings } from '../kernel/engine/tree.js';
|
|
4
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
4
5
|
function isImageBlob(rec) {
|
|
5
6
|
if (rec.type === 'image' || rec.type === 'media') {
|
|
6
7
|
return true;
|
|
@@ -44,3 +45,11 @@ async function tapeGemini(value, canary) {
|
|
|
44
45
|
return redactCanaryInTree(await scrubGemini(value), canary);
|
|
45
46
|
}
|
|
46
47
|
export { tapeGemini };
|
|
48
|
+
exposeForTests('gemini-tape', {
|
|
49
|
+
isImageBlob,
|
|
50
|
+
scrubEntry,
|
|
51
|
+
scrubRecord,
|
|
52
|
+
scrubGemini,
|
|
53
|
+
redactCanaryInTree,
|
|
54
|
+
tapeGemini,
|
|
55
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
1
2
|
const SECRET_HEADER = /key|auth|cookie|secret|token/i;
|
|
2
3
|
function tapeHeaderValue(key, value) {
|
|
3
4
|
if (SECRET_HEADER.test(key)) {
|
|
@@ -46,3 +47,4 @@ function tapFetch(tap, send = fetch) {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
export { tapFetch };
|
|
50
|
+
exposeForTests('google-tap', { tapeHeaderValue, tapeHeaders, throwRow, tapFetch });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TheorumError } from '../guardrails/error.js';
|
|
2
2
|
import { getTool } from '../kernel/registry/catalog.js';
|
|
3
3
|
import { getStructured } from '../kernel/registry/schemas.js';
|
|
4
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
4
5
|
function camelToSnake(key) {
|
|
5
6
|
return key.replaceAll(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
6
7
|
}
|
|
@@ -151,3 +152,18 @@ function toInteractionsBody(req) {
|
|
|
151
152
|
return toGoogleValue(camel);
|
|
152
153
|
}
|
|
153
154
|
export { camelToSnake, toInteractionsBody };
|
|
155
|
+
exposeForTests('interactions', {
|
|
156
|
+
camelToSnake,
|
|
157
|
+
toGoogleValue,
|
|
158
|
+
wirePart,
|
|
159
|
+
userInputStep,
|
|
160
|
+
historyStep,
|
|
161
|
+
systemHoldsUserInput,
|
|
162
|
+
jsonResponseFormat,
|
|
163
|
+
attachResponseFormat,
|
|
164
|
+
attachSpeechConfig,
|
|
165
|
+
inputStepsFromRequest,
|
|
166
|
+
applyOptionalRequestFields,
|
|
167
|
+
baseInteractionsBody,
|
|
168
|
+
toInteractionsBody,
|
|
169
|
+
});
|