theorum 1.0.0 → 1.1.3

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 (204) hide show
  1. package/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. package/esm/src/interface/types.js +0 -13
@@ -6,35 +6,50 @@
6
6
  import { sanitizeTurnBlobsForProfile } from '../kernel/registry/attachments.js';
7
7
  import { getProfile } from '../kernel/registry/profiles.js';
8
8
  import { applySpans } from '../observability/spans.js';
9
+ import { guardrailFromHits } from './events.js';
10
+ import { hitFromSpan } from './hits.js';
9
11
  import { injectionSpans } from './injection.js';
12
+ import { detectionForTrust, resolveGuardrailPolicy } from './policy.js';
10
13
  import { sensitiveSpans } from './sensitive.js';
11
- /** Sanitize one text value using prompt-injection and sensitive-data detectors. */
12
- function sanitizeText(text, options) {
14
+ /**
15
+ * Detect and redact injection / sensitive spans. Returns hits for observability
16
+ * (rule + offsets + optional `match` preview for debugging).
17
+ */
18
+ function detectText(text, options) {
13
19
  const sanitizeInput = options?.sanitizeInput ?? true;
14
20
  const redactSensitive = options?.redactSensitive ?? true;
15
21
  if (!sanitizeInput && !redactSensitive) {
16
- return text;
22
+ return { text, hits: [] };
17
23
  }
18
24
  const spans = [
19
25
  ...(sanitizeInput ? injectionSpans(text) : []),
20
26
  ...(redactSensitive ? sensitiveSpans(text) : []),
21
27
  ];
22
- return applySpans(text, spans);
28
+ const hits = spans.map((span) => hitFromSpan(text, span, span.kind === 'injection' ? 'sanitize.injection' : 'sanitize.sensitive', 'high'));
29
+ return { text: applySpans(text, spans), hits };
30
+ }
31
+ /** Sanitize one text value using prompt-injection and sensitive-data detectors. */
32
+ function sanitizeText(text, options) {
33
+ return detectText(text, options).text;
23
34
  }
24
35
  /** Redact only sensitive data (credentials, PII) — skip injection patterns. */
25
36
  function redactSensitiveOnly(text) {
26
- const spans = sensitiveSpans(text);
27
- if (spans.length === 0)
28
- return text;
29
- return applySpans(text, spans);
37
+ return detectText(text, { sanitizeInput: false, redactSensitive: true }).text;
30
38
  }
31
- function sanitizeSlots(slots, options) {
39
+ function appendHits(into, hits) {
40
+ for (const hit of hits) {
41
+ into.push(hit);
42
+ }
43
+ }
44
+ function sanitizeSlots(slots, options, hits) {
32
45
  if (!slots) {
33
46
  return slots;
34
47
  }
35
48
  const out = {};
36
49
  for (const [key, value] of Object.entries(slots)) {
37
- out[key] = sanitizeText(value, options);
50
+ const detected = detectText(value, options);
51
+ appendHits(hits, detected.hits);
52
+ out[key] = detected.text;
38
53
  }
39
54
  return out;
40
55
  }
@@ -50,85 +65,162 @@ function sanitizeProjectId(id) {
50
65
  }
51
66
  return trimmed;
52
67
  }
53
- function sanitizeRepair(repair, options) {
68
+ function sanitizeRepair(repair, options, hits) {
54
69
  if (!repair) {
55
70
  return repair;
56
71
  }
72
+ const previous = detectText(repair.previousOutput, options);
73
+ const rejection = detectText(repair.rejection, options);
74
+ appendHits(hits, previous.hits);
75
+ appendHits(hits, rejection.hits);
76
+ let guidance = repair.guidance;
77
+ if (guidance) {
78
+ const detected = detectText(guidance, options);
79
+ appendHits(hits, detected.hits);
80
+ guidance = detected.text;
81
+ }
57
82
  return {
58
- previousOutput: sanitizeText(repair.previousOutput, options),
59
- rejection: sanitizeText(repair.rejection, options),
60
- guidance: repair.guidance ? sanitizeText(repair.guidance, options) : undefined,
83
+ previousOutput: previous.text,
84
+ rejection: rejection.text,
85
+ ...(guidance ? { guidance } : {}),
61
86
  };
62
87
  }
63
- function sanitizeHistory(history, options) {
64
- if (!history) {
65
- return history;
66
- }
67
- return history.map((m) => ({
68
- role: m.role,
69
- ...(m.content !== undefined ? { content: sanitizeText(m.content, options) } : {}),
70
- ...(m.parts
71
- ? {
72
- parts: m.parts.map((p) => p.type === 'text' ? { ...p, text: sanitizeText(p.text, options) } : p),
73
- }
74
- : {}),
75
- ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
76
- ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
77
- ...(m.name ? { name: m.name } : {}),
78
- ...(m.metadata ? { metadata: m.metadata } : {}),
79
- }));
88
+ /**
89
+ * Sanitize the text of each history message; tool calls, ids, and metadata pass
90
+ * through untouched.
91
+ *
92
+ * Exported because every path that injects messages into a turn needs it — turn
93
+ * history, and host steer injects mid-turn. A second copy would drift.
94
+ */
95
+ function sanitizeHistory(history, options, hits = []) {
96
+ return history.map((m) => {
97
+ let content = m.content;
98
+ if (content !== undefined) {
99
+ const detected = detectText(content, options);
100
+ appendHits(hits, detected.hits);
101
+ content = detected.text;
102
+ }
103
+ let parts = m.parts;
104
+ if (parts) {
105
+ parts = parts.map((p) => {
106
+ if (p.type !== 'text') {
107
+ return p;
108
+ }
109
+ const detected = detectText(p.text, options);
110
+ appendHits(hits, detected.hits);
111
+ return { ...p, text: detected.text };
112
+ });
113
+ }
114
+ return {
115
+ role: m.role,
116
+ ...(content !== undefined ? { content } : {}),
117
+ ...(parts ? { parts } : {}),
118
+ ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
119
+ ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
120
+ ...(m.name ? { name: m.name } : {}),
121
+ ...(m.metadata ? { metadata: m.metadata } : {}),
122
+ };
123
+ });
80
124
  }
81
- function guardrailTextOptions(profileId) {
82
- let profileGuardrails;
125
+ /**
126
+ * Detection switches for one profile at one trust level.
127
+ *
128
+ * Falls back to full detection when the profile is not registered yet, so an
129
+ * unknown id never silently disables guardrails.
130
+ */
131
+ function detectionForProfile(profileId, trust) {
132
+ let spec;
83
133
  try {
84
- profileGuardrails = getProfile(profileId)?.guardrails;
134
+ spec = getProfile(profileId)?.guardrails;
85
135
  }
86
136
  catch {
87
137
  // If profile not registered yet, default to full guardrails.
88
138
  }
89
- return {
90
- sanitizeInput: profileGuardrails?.sanitizeInput ?? true,
91
- redactSensitive: profileGuardrails?.redactSensitive ?? true,
92
- };
139
+ return detectionForTrust(resolveGuardrailPolicy(spec), trust);
140
+ }
141
+ function pushStageEvent(events, stage, trust, hits) {
142
+ const event = guardrailFromHits(stage, trust, hits, 'redact');
143
+ if (event) {
144
+ events.push(event);
145
+ }
93
146
  }
94
- /** Sanitize user-controlled text fields; leave attachments/voice untouched. */
95
- function sanitizeTurnRequestText(req, options) {
147
+ /**
148
+ * Sanitize user-controlled text fields; leave attachments/voice untouched.
149
+ *
150
+ * Returns `{ type: 'guardrail' }` events for stages that redacted something.
151
+ * Clean surfaces emit nothing.
152
+ *
153
+ * `req.system` is host-assembled per turn — it interpolates retrieval and user
154
+ * data, so it is treated as `assembled`, not trusted. `identity.system` never
155
+ * reaches this path and stays verbatim.
156
+ */
157
+ function sanitizeTurnRequestText(req, profileId) {
158
+ const untrusted = detectionForProfile(profileId, 'untrusted');
159
+ const assembled = detectionForProfile(profileId, 'assembled');
96
160
  const input = req.input ?? {};
161
+ const events = [];
162
+ const inputHits = [];
163
+ const historyHits = [];
164
+ const systemHits = [];
97
165
  const { text: rawText } = input;
98
166
  let text = rawText;
99
167
  if (rawText !== undefined) {
100
- text = sanitizeText(rawText, options);
168
+ const detected = detectText(rawText, untrusted);
169
+ appendHits(inputHits, detected.hits);
170
+ text = detected.text;
101
171
  }
102
172
  let system = req.system;
103
173
  if (system !== undefined) {
104
- system = sanitizeText(system, options);
174
+ const detected = detectText(system, assembled);
175
+ appendHits(systemHits, detected.hits);
176
+ system = detected.text;
105
177
  }
178
+ const slots = sanitizeSlots(input.slots, untrusted, inputHits);
179
+ const repair = sanitizeRepair(input.repair, untrusted, inputHits);
180
+ const history = input.history
181
+ ? sanitizeHistory(input.history, untrusted, historyHits)
182
+ : undefined;
183
+ pushStageEvent(events, 'input', 'untrusted', inputHits);
184
+ pushStageEvent(events, 'history', 'untrusted', historyHits);
185
+ pushStageEvent(events, 'system', 'assembled', systemHits);
106
186
  return {
107
- ...req,
108
- system,
109
- projectId: sanitizeProjectId(req.projectId),
110
- input: {
111
- ...input,
112
- text,
113
- slots: sanitizeSlots(input.slots, options),
114
- repair: sanitizeRepair(input.repair, options),
115
- history: sanitizeHistory(input.history, options),
187
+ request: {
188
+ ...req,
189
+ system,
190
+ projectId: sanitizeProjectId(req.projectId),
191
+ input: {
192
+ ...input,
193
+ text,
194
+ slots,
195
+ repair,
196
+ history,
197
+ },
116
198
  },
199
+ events,
117
200
  };
118
201
  }
119
202
  /** Sanitize all user-controlled text and blobs in a turn request. */
120
203
  function sanitizeTurnRequest(req) {
121
- const options = guardrailTextOptions(req.profile);
122
- const textSafe = sanitizeTurnRequestText(req, options);
204
+ return sanitizeTurnRequestWithEvents(req).request;
205
+ }
206
+ /**
207
+ * Sanitize a turn request and return guardrail events for any redactionsactions spans.
208
+ * Attachments/voice are validated but do not emit content-span events.
209
+ */
210
+ function sanitizeTurnRequestWithEvents(req) {
211
+ const { request: textSafe, events } = sanitizeTurnRequestText(req, req.profile);
123
212
  const input = textSafe.input ?? {};
124
213
  const { attachments, voice } = sanitizeTurnBlobsForProfile(req.profile, input.attachments, input.voice);
125
214
  return {
126
- ...textSafe,
127
- input: {
128
- ...input,
129
- attachments,
130
- voice,
215
+ request: {
216
+ ...textSafe,
217
+ input: {
218
+ ...input,
219
+ attachments,
220
+ voice,
221
+ },
131
222
  },
223
+ events,
132
224
  };
133
225
  }
134
226
  /**
@@ -142,9 +234,9 @@ function sanitizeTurnRequestForTrace(req) {
142
234
  catch (err) {
143
235
  const message = err instanceof Error ? err.message : String(err);
144
236
  return {
145
- request: sanitizeTurnRequestText(req, guardrailTextOptions(req.profile)),
237
+ request: sanitizeTurnRequestText(req, req.profile).request,
146
238
  sanitizeError: message,
147
239
  };
148
240
  }
149
241
  }
150
- export { PROJECT_ID_MAX, redactSensitiveOnly, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, sanitizeTurnRequestForTrace, };
242
+ export { detectionForProfile, detectText, PROJECT_ID_MAX, redactSensitiveOnly, sanitizeHistory, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, sanitizeTurnRequestForTrace, sanitizeTurnRequestWithEvents, };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Serialization for guardrail scanning.
3
+ *
4
+ * Detectors work on strings, so non-text event payloads (structured output, tool
5
+ * arguments, grounding metadata) must be flattened before they can be inspected.
6
+ * A guardrail must never be the thing that throws, so this never propagates a
7
+ * serializer error: cycles and bigints are represented rather than fatal, and a
8
+ * payload that still cannot be rendered is reported so the caller can fail closed.
9
+ *
10
+ * @module
11
+ */
12
+ /** Marker substituted for a repeated reference so a cycle terminates. */
13
+ declare const CIRCULAR = "[circular]";
14
+ export interface ScanText {
15
+ text: string;
16
+ /** True when the payload could not be rendered and was not inspected. */
17
+ unscannable: boolean;
18
+ }
19
+ /**
20
+ * Flatten an arbitrary payload to text for detector scanning.
21
+ *
22
+ * Cycles collapse to `[circular]` and bigints render as digits, so the common
23
+ * unserializable shapes still get inspected instead of aborting the turn. Only a
24
+ * payload that defeats that (a throwing `toJSON`, for instance) comes back
25
+ * `unscannable`.
26
+ */
27
+ declare function textForScan(value: unknown): ScanText;
28
+ /**
29
+ * Scan-ready text, discarding the unscannable signal.
30
+ *
31
+ * For callers whose only question is "does this contain X" and for whom an
32
+ * unrenderable payload is the same as no match.
33
+ */
34
+ declare function scanTextOf(value: unknown): string;
35
+ export { CIRCULAR, scanTextOf, textForScan };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Serialization for guardrail scanning.
3
+ *
4
+ * Detectors work on strings, so non-text event payloads (structured output, tool
5
+ * arguments, grounding metadata) must be flattened before they can be inspected.
6
+ * A guardrail must never be the thing that throws, so this never propagates a
7
+ * serializer error: cycles and bigints are represented rather than fatal, and a
8
+ * payload that still cannot be rendered is reported so the caller can fail closed.
9
+ *
10
+ * @module
11
+ */
12
+ /** Marker substituted for a repeated reference so a cycle terminates. */
13
+ const CIRCULAR = '[circular]';
14
+ /**
15
+ * Flatten an arbitrary payload to text for detector scanning.
16
+ *
17
+ * Cycles collapse to `[circular]` and bigints render as digits, so the common
18
+ * unserializable shapes still get inspected instead of aborting the turn. Only a
19
+ * payload that defeats that (a throwing `toJSON`, for instance) comes back
20
+ * `unscannable`.
21
+ */
22
+ function textForScan(value) {
23
+ if (value === undefined) {
24
+ return { text: '', unscannable: false };
25
+ }
26
+ if (typeof value === 'string') {
27
+ return { text: value, unscannable: false };
28
+ }
29
+ const seen = new WeakSet();
30
+ try {
31
+ const json = JSON.stringify(value, (_key, val) => {
32
+ if (typeof val === 'bigint') {
33
+ return val.toString();
34
+ }
35
+ if (typeof val === 'object' && val !== null) {
36
+ if (seen.has(val)) {
37
+ return CIRCULAR;
38
+ }
39
+ seen.add(val);
40
+ }
41
+ return val;
42
+ });
43
+ return { text: json ?? '', unscannable: false };
44
+ }
45
+ catch {
46
+ return { text: '', unscannable: true };
47
+ }
48
+ }
49
+ /**
50
+ * Scan-ready text, discarding the unscannable signal.
51
+ *
52
+ * For callers whose only question is "does this contain X" and for whom an
53
+ * unrenderable payload is the same as no match.
54
+ */
55
+ function scanTextOf(value) {
56
+ return textForScan(value).text;
57
+ }
58
+ export { CIRCULAR, scanTextOf, textForScan };
@@ -5,6 +5,13 @@
5
5
  *
6
6
  * @module
7
7
  */
8
+ /** lexicon-exempt-file: adversarial harness helpers — not runtime user or model copy (P2) */
8
9
  import "../../_dnt.polyfills.js";
9
10
  export type { CanaryEgressAttack, CanaryEgressCatalogEntry, InboundFuzzPayload, InboundFuzzResult, LiveAttack, } from './corpus/mod.js';
10
11
  export { buildCanaryEgressAttacks, buildLiveAttacks, canaryEgressCatalog, FIXED_CANARY, filterLiveAttacks, inboundFuzzPayloads, inboundPayloadByName, runInboundGuardrailFuzz, summarizeAttackBank, } from './corpus/mod.js';
12
+ export type { CorpusSample, CorpusSource } from './eval/corpus.js';
13
+ export { createCorpusCache, parseLabelledCsv, recordsFromYaml, SOURCES } from './eval/corpus.js';
14
+ export type { EvalOptions, EvalReport } from './eval/mod.js';
15
+ export { DETECTORS, formatReport, runGuardrailEval } from './eval/mod.js';
16
+ export type { DetectorScore, EvalDetector } from './eval/score.js';
17
+ export { formatScores, scoreAll, scoreDetector } from './eval/score.js';
@@ -5,5 +5,9 @@
5
5
  *
6
6
  * @module
7
7
  */
8
+ /** lexicon-exempt-file: adversarial harness helpers — not runtime user or model copy (P2) */
8
9
  import "../../_dnt.polyfills.js";
9
10
  export { buildCanaryEgressAttacks, buildLiveAttacks, canaryEgressCatalog, FIXED_CANARY, filterLiveAttacks, inboundFuzzPayloads, inboundPayloadByName, runInboundGuardrailFuzz, summarizeAttackBank, } from './corpus/mod.js';
11
+ export { createCorpusCache, parseLabelledCsv, recordsFromYaml, SOURCES } from './eval/corpus.js';
12
+ export { DETECTORS, formatReport, runGuardrailEval } from './eval/mod.js';
13
+ export { formatScores, scoreAll, scoreDetector } from './eval/score.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Contract-failure error class for THEORUM.
3
+ *
4
+ * Lives in its own module so `lexicon.ts` can throw it without importing
5
+ * `error.ts` (which resolves public copy through the lexicon).
6
+ *
7
+ * @module
8
+ */
9
+ /** Error class used for expected THEORUM contract failures. */
10
+ export declare class TheorumError extends Error {
11
+ constructor(message?: string, options?: ErrorOptions);
12
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Contract-failure error class for THEORUM.
3
+ *
4
+ * Lives in its own module so `lexicon.ts` can throw it without importing
5
+ * `error.ts` (which resolves public copy through the lexicon).
6
+ *
7
+ * @module
8
+ */
9
+ /** Error class used for expected THEORUM contract failures. */
10
+ export class TheorumError extends Error {
11
+ constructor(message = '', options) {
12
+ super(message, options);
13
+ this.name = 'TheorumError';
14
+ }
15
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Directive detection for tool ingress.
3
+ *
4
+ * Tool results carry a different threat than user text. The jailbreak phrasings in
5
+ * `injection.ts` name the thing they attack — "ignore previous instructions",
6
+ * "reveal your system prompt" — and real indirect injection rarely does. It reads
7
+ * like a status update or a helpful next step, and pattern-matching for the word
8
+ * "instructions" misses all of it.
9
+ *
10
+ * What is anomalous in *data* is content that behaves like an instruction: naming
11
+ * a tool the agent can call, issuing an imperative at the agent, or claiming an
12
+ * authority the content does not have.
13
+ *
14
+ * A signal only counts when it co-occurs with a concrete external destination —
15
+ * an address or URL. Directive language alone is far too common in legitimate
16
+ * output to act on. These signals raise the turn's taint rather than rewriting the text. A page
17
+ * documenting an email API legitimately says "call send_email"; redacting that
18
+ * would corrupt content the model needs. Being wrong here should cost a refused
19
+ * write — recoverable and visible — not silently damaged input.
20
+ *
21
+ * @module
22
+ */
23
+ import type { AdvisoryLevel, GuardrailHit } from './types.js';
24
+ /** Rule ids emitted by tool-ingress directive detection. */
25
+ export declare const DIRECTIVE_RULES: {
26
+ readonly toolName: "tool_result.names-callable-tool";
27
+ readonly imperative: "tool_result.imperative";
28
+ readonly authority: "tool_result.authority-claim";
29
+ };
30
+ /**
31
+ * Detect instruction-shaped content in a tool result.
32
+ *
33
+ * `callableTools` is the set the model can actually invoke this turn. A result
34
+ * naming one is the highest-precision signal available — ordinary data has no
35
+ * reason to name the agent's tools, and no generic content filter can check it
36
+ * because it requires the turn's registry.
37
+ */
38
+ declare function directiveHits(text: string, callableTools?: readonly string[]): GuardrailHit[];
39
+ /** True when a result looked like it was trying to steer the agent. */
40
+ declare function looksDirective(hits: GuardrailHit[]): boolean;
41
+ /**
42
+ * Strength of the signals, read off the hits rather than invented.
43
+ *
44
+ * Naming a tool the model can call is the sharpest signal available, so it alone
45
+ * reaches `high`; so does agreement between two different signal kinds.
46
+ */
47
+ declare function advisoryLevel(hits: GuardrailHit[]): AdvisoryLevel;
48
+ export { advisoryLevel, directiveHits, looksDirective };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Directive detection for tool ingress.
3
+ *
4
+ * Tool results carry a different threat than user text. The jailbreak phrasings in
5
+ * `injection.ts` name the thing they attack — "ignore previous instructions",
6
+ * "reveal your system prompt" — and real indirect injection rarely does. It reads
7
+ * like a status update or a helpful next step, and pattern-matching for the word
8
+ * "instructions" misses all of it.
9
+ *
10
+ * What is anomalous in *data* is content that behaves like an instruction: naming
11
+ * a tool the agent can call, issuing an imperative at the agent, or claiming an
12
+ * authority the content does not have.
13
+ *
14
+ * A signal only counts when it co-occurs with a concrete external destination —
15
+ * an address or URL. Directive language alone is far too common in legitimate
16
+ * output to act on. These signals raise the turn's taint rather than rewriting the text. A page
17
+ * documenting an email API legitimately says "call send_email"; redacting that
18
+ * would corrupt content the model needs. Being wrong here should cost a refused
19
+ * write — recoverable and visible — not silently damaged input.
20
+ *
21
+ * @module
22
+ */
23
+ import { normalizeForDetection } from './normalize.js';
24
+ /** Rule ids emitted by tool-ingress directive detection. */
25
+ export const DIRECTIVE_RULES = {
26
+ toolName: 'tool_result.names-callable-tool',
27
+ imperative: 'tool_result.imperative',
28
+ authority: 'tool_result.authority-claim',
29
+ };
30
+ /**
31
+ * Imperatives aimed at an agent rather than a reader.
32
+ *
33
+ * Bounded quantifiers throughout: tool results can be large, and an unbounded
34
+ * scanner on attacker-controlled bytes is a denial-of-service surface.
35
+ */
36
+ const IMPERATIVES = [
37
+ /\b(?:next|final|remaining)\s+steps?\s*[:—-]/gi,
38
+ /\byou\s+(?:must|should|need\s+to|are\s+required\s+to)\s+(?:now\s+)?\w{2,20}/gi,
39
+ /\bto\s+(?:complete|finish|fulfil|fulfill)\s+(?:this|the)\s+\w{2,20}/gi,
40
+ /\b(?:call|invoke|run|execute)\s+(?:the\s+)?[\w.-]{2,40}\s+(?:tool|function)\b/gi,
41
+ /\b(?:send|forward|email|transfer|upload|post)\s+(?:the\s+|all\s+|your\s+)?[\w\s]{2,40}\s+to\s+\S{3,80}/gi,
42
+ /\b(?:delete|remove|drop|purge)\s+(?:the\s+|all\s+)?[\w\s]{2,40}\b/gi,
43
+ ];
44
+ /**
45
+ * A concrete external destination — the thing exfiltration needs and ordinary
46
+ * process prose almost never carries.
47
+ *
48
+ * This is the discriminator. Directive language on its own is everywhere in
49
+ * legitimate tool output: documentation says "you must be an admin", support
50
+ * articles say "to remove a user", status reports say "the user has approved".
51
+ * Measured on a benign sample, directive signals alone produced false positives
52
+ * on most of it. Pairing a signal with a destination removed all of them.
53
+ */
54
+ const EXFIL_TARGET = /\b[\w.+-]{1,64}@[\w-]{1,63}(?:\.[\w-]{1,63}){1,4}\b|\bhttps?:\/\/\S{4,120}/i;
55
+ /** Claims of permission or provenance the content cannot actually hold. */
56
+ const AUTHORITY = [
57
+ /\b(?:the\s+)?user\s+has\s+(?:already\s+)?(?:approved|authorised|authorized|confirmed|requested)\b/gi,
58
+ /\b(?:system|admin|operator)\s+(?:note|notice|message|override|instruction)s?\s*[:—-]/gi,
59
+ /\bon\s+behalf\s+of\s+the\s+(?:user|operator|admin)\b/gi,
60
+ /\bthis\s+(?:is|was)\s+(?:pre-?)?(?:approved|authorised|authorized)\b/gi,
61
+ ];
62
+ function matches(patterns, text) {
63
+ return patterns.some((pattern) => {
64
+ pattern.lastIndex = 0;
65
+ return pattern.test(text);
66
+ });
67
+ }
68
+ /** Word-boundary match for a tool name, escaped so registry names cannot inject. */
69
+ function mentionsTool(text, tool) {
70
+ if (tool.length < 3) {
71
+ return false;
72
+ }
73
+ const escaped = tool.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
+ return new RegExp(`(?:^|[^\\w-])${escaped}(?:$|[^\\w-])`, 'i').test(text);
75
+ }
76
+ /**
77
+ * Detect instruction-shaped content in a tool result.
78
+ *
79
+ * `callableTools` is the set the model can actually invoke this turn. A result
80
+ * naming one is the highest-precision signal available — ordinary data has no
81
+ * reason to name the agent's tools, and no generic content filter can check it
82
+ * because it requires the turn's registry.
83
+ */
84
+ function directiveHits(text, callableTools = []) {
85
+ if (!text || !EXFIL_TARGET.test(text)) {
86
+ // No destination, no exfiltration. Action-shaped attacks that carry no target
87
+ // are left to the taint gate, which does not depend on reading the content.
88
+ return [];
89
+ }
90
+ const normalized = normalizeForDetection(text);
91
+ const hits = [];
92
+ // One hit per named tool — several names is a stronger signal than one.
93
+ for (const _tool of callableTools.filter((tool) => mentionsTool(normalized, tool))) {
94
+ hits.push({ rule: DIRECTIVE_RULES.toolName, severity: 'high' });
95
+ }
96
+ if (matches(IMPERATIVES, normalized)) {
97
+ hits.push({ rule: DIRECTIVE_RULES.imperative, severity: 'medium' });
98
+ }
99
+ if (matches(AUTHORITY, normalized)) {
100
+ hits.push({ rule: DIRECTIVE_RULES.authority, severity: 'medium' });
101
+ }
102
+ return hits;
103
+ }
104
+ /** True when a result looked like it was trying to steer the agent. */
105
+ function looksDirective(hits) {
106
+ return hits.length > 0;
107
+ }
108
+ /**
109
+ * Strength of the signals, read off the hits rather than invented.
110
+ *
111
+ * Naming a tool the model can call is the sharpest signal available, so it alone
112
+ * reaches `high`; so does agreement between two different signal kinds.
113
+ */
114
+ function advisoryLevel(hits) {
115
+ if (hits.length === 0) {
116
+ return 'none';
117
+ }
118
+ const kinds = new Set(hits.map((hit) => hit.rule));
119
+ if (kinds.has(DIRECTIVE_RULES.toolName) || kinds.size > 1) {
120
+ return 'high';
121
+ }
122
+ return 'elevated';
123
+ }
124
+ export { advisoryLevel, directiveHits, looksDirective };