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.
Files changed (37) hide show
  1. package/esm/src/cli/commands/bench.d.ts +20 -0
  2. package/esm/src/cli/commands/bench.js +454 -0
  3. package/esm/src/cli/commands/fuzz-guardrails.d.ts +9 -0
  4. package/esm/src/cli/commands/fuzz-guardrails.js +440 -0
  5. package/esm/src/cli/index.js +20 -1
  6. package/esm/src/guardrails/injection.d.ts +0 -1
  7. package/esm/src/guardrails/injection.js +105 -12
  8. package/esm/src/guardrails/normalize.d.ts +11 -0
  9. package/esm/src/guardrails/normalize.js +110 -0
  10. package/esm/src/guardrails/sanitize.d.ts +10 -2
  11. package/esm/src/guardrails/sanitize.js +27 -1
  12. package/esm/src/guardrails/sensitive.js +5 -3
  13. package/esm/src/kernel/engine/boundary.js +10 -10
  14. package/esm/src/kernel/engine/runner/mod.js +4 -1
  15. package/esm/src/kernel/engine/runner/tools.js +11 -5
  16. package/esm/src/kernel/types.d.ts +1 -1
  17. package/esm/src/observability/trace-record.d.ts +1 -0
  18. package/esm/src/observability/trace-record.js +4 -4
  19. package/esm/src/providers/create-provider.js +2 -0
  20. package/esm/src/providers/expose-for-tests.d.ts +1 -0
  21. package/esm/src/providers/expose-for-tests.js +21 -0
  22. package/esm/src/providers/gemini-tape.js +9 -0
  23. package/esm/src/providers/google-tap.js +2 -0
  24. package/esm/src/providers/interactions.js +16 -0
  25. package/esm/src/providers/keys.js +11 -0
  26. package/esm/src/providers/openrouter-payload.d.ts +9 -2
  27. package/esm/src/providers/openrouter-payload.js +23 -7
  28. package/esm/src/providers/openrouter.js +101 -140
  29. package/esm/src/providers/pcm.js +2 -0
  30. package/esm/src/providers/provider.js +13 -0
  31. package/esm/src/providers/speech.js +10 -0
  32. package/esm/src/providers/sse.js +2 -0
  33. package/esm/src/streaming/mod.d.ts +7 -0
  34. package/esm/src/streaming/mod.js +7 -0
  35. package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
  36. package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
  37. 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 = /<\uFF5C(?:end\u2581of\u2581sentence|begin\u2581of\u2581sentence)\uFF5C>/g;
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
- /** Detect prompt-injection spans in a string. */
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
- return [...injectionSpansOn(text), ...typo, ...encodedSpans(text), ...spacedSpans(text)];
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-[A-Za-z0-9]{20,}\b/g;
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[ -]*?){13,19}\b/g;
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
- return haystack(event).includes(canary);
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
- const safe = sanitizeTurnRequest(req);
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
- return res.finding;
8
+ raw = res.finding;
7
9
  }
8
- if (res.data) {
9
- return JSON.stringify(res.data);
10
+ else if (res.data) {
11
+ raw = JSON.stringify(res.data);
10
12
  }
11
- return 'ok';
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 loaded = await loader({
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. */
@@ -108,6 +108,7 @@ declare function buildRecord(args: {
108
108
  canary?: string;
109
109
  system?: string;
110
110
  generation?: ResolvedGeneration;
111
+ sanitizedReq?: TurnRequest;
111
112
  }): Promise<TraceRecord>;
112
113
  export type { TraceRecord };
113
114
  export { buildRecord };
@@ -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 = sanitizeText(event.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 = sanitizeText(event.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
+ });