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
@@ -4,33 +4,84 @@
4
4
  * @module
5
5
  */
6
6
  import { scanTextForCanaryLeak } from './canary.js';
7
+ import { hitFromSpan } from './hits.js';
7
8
  import { injectionSpans } from './injection.js';
8
9
  import { sensitiveSpans } from './sensitive.js';
9
- const SYSTEM_BOUNDARY = /This turn's canary is|<\/?user_data>|Untrusted user content is inside/i;
10
- /** Default egress enforce canary leak, sensitive echo, fence markers, injection echo. */
11
- function standardEgressEnforce(context) {
12
- const { text, canary } = context;
10
+ import { textForScan } from './serialize.js';
11
+ const SYSTEM_BOUNDARY = /This turn's canary is|<\/?user_data>/i; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
12
+ /** Rule ids emitted by the bundled outbound policy. */
13
+ export const EGRESS_RULES = {
14
+ canary: 'egress.canary-leak',
15
+ sensitive: 'egress.sensitive-echo',
16
+ boundary: 'egress.system-boundary',
17
+ injection: 'egress.injection-echo', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
18
+ /** Payload could not be rendered for inspection — released output is unverified. */
19
+ unscannable: 'egress.unscannable', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
20
+ /** The host policy threw instead of returning a verdict. */
21
+ enforcerError: 'egress.enforcer-error', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
22
+ };
23
+ function hitsFromSpans(text, spans, rule, severity) {
24
+ return spans.map((span) => hitFromSpan(text, span, rule, severity));
25
+ }
26
+ /** Hits from the bundled outbound policy (canary / sensitive / boundary / injection). */
27
+ function collectEgressHits(text, canary) {
13
28
  const hits = [];
14
29
  if (canary && scanTextForCanaryLeak(text, canary)) {
15
- hits.push('canary');
30
+ // Never put the live canary token into match — placeholder only.
31
+ hits.push({ rule: EGRESS_RULES.canary, severity: 'high', match: '[canary]' });
32
+ }
33
+ hits.push(...hitsFromSpans(text, sensitiveSpans(text), EGRESS_RULES.sensitive, 'high')); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
34
+ const boundary = SYSTEM_BOUNDARY.exec(text);
35
+ if (boundary && boundary.index !== undefined) {
36
+ hits.push(hitFromSpan(text, { start: boundary.index, end: boundary.index + boundary[0].length }, EGRESS_RULES.boundary, 'medium'));
16
37
  }
17
- if (sensitiveSpans(text).length > 0) {
18
- hits.push('sensitive');
38
+ hits.push(...hitsFromSpans(text, injectionSpans(text), EGRESS_RULES.injection, 'medium')); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
39
+ return hits;
40
+ }
41
+ /** Distinct rule ids in a hit list, in first-seen order — for rejection copy. */
42
+ function hitRules(hits) {
43
+ return [...new Set(hits.map((hit) => hit.rule))];
44
+ }
45
+ /** Default egress enforce — canary leak, sensitive echo, fence markers, injection echo. */
46
+ function standardEgressEnforce(payload, context) {
47
+ const hits = collectEgressHits(payload.text, context.canary);
48
+ if (payload.structured !== undefined) {
49
+ const structured = textForScan(payload.structured);
50
+ if (structured.unscannable) {
51
+ // Cannot inspect it, so cannot vouch for it. Fail closed.
52
+ hits.push({ rule: EGRESS_RULES.unscannable, severity: 'high' }); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
53
+ }
54
+ else {
55
+ hits.push(...collectEgressHits(structured.text, context.canary));
56
+ }
19
57
  }
20
- if (SYSTEM_BOUNDARY.test(text)) {
21
- hits.push('system_boundary');
58
+ if (hits.length === 0) {
59
+ return { action: 'allow' };
22
60
  }
23
- if (injectionSpans(text).length > 0) {
24
- hits.push('injection_echo');
61
+ return {
62
+ action: 'block', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
63
+ hits,
64
+ rejection: `Egress blocked: ${hitRules(hits).join(', ')}`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
65
+ };
66
+ }
67
+ /**
68
+ * Run a host policy without letting it break the turn.
69
+ *
70
+ * A policy that throws has reached no decision, so it cannot vouch for the output:
71
+ * the failure becomes a `block`, not a pass. The turn then follows the profile's
72
+ * ordinary `onBlock` handling instead of surfacing a raw host stack trace.
73
+ */
74
+ async function runEnforcer(enforce, payload, context) {
75
+ try {
76
+ return await enforce(payload, context);
25
77
  }
26
- if (hits.length > 0) {
78
+ catch (err) {
79
+ const detail = err instanceof Error ? err.message : String(err);
27
80
  return {
28
- blocked: true,
29
- text: '',
30
- hits,
31
- rejectionMessage: `Egress blocked: ${hits.join(', ')}`,
81
+ action: 'block',
82
+ hits: [{ rule: EGRESS_RULES.enforcerError, severity: 'high' }],
83
+ rejection: `Egress policy failed to reach a decision: ${detail}`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
32
84
  };
33
85
  }
34
- return { blocked: false, text };
35
86
  }
36
- export { standardEgressEnforce };
87
+ export { collectEgressHits, hitRules, runEnforcer, standardEgressEnforce };
@@ -2,34 +2,25 @@
2
2
  * Public-safe error mapping for THEORUM.
3
3
  *
4
4
  * Kernel internals may contain provider status text, tool names, or exception
5
- * details. This module maps those failures to stable user-safe strings.
5
+ * details. This module maps those failures to stable user-safe strings from the
6
+ * kernel lexicon (`public.*` keys) so hosts can override them via
7
+ * `overrideLexicon`.
6
8
  *
7
9
  * @module
8
10
  */
9
- /** Error class used for expected THEORUM contract failures. */
10
- declare class TheorumError extends Error {
11
- constructor(message?: string, options?: ErrorOptions);
12
- }
11
+ import { TheorumError } from './theorum-error.js';
13
12
  /** Internal marker for provider or transport failure. */
14
13
  declare const UPSTREAM_FAILED = "upstream failed";
15
- /** Generic safe fallback shown when details must not be surfaced. */
16
- declare const PUBLIC_GENERIC = "Something went wrong. Try again.";
17
- /** Safe copy for transient provider unavailability. */
18
- declare const PUBLIC_UNAVAILABLE = "The model is unavailable. Try again.";
19
- /** Safe copy for canary or egress disclosure violations. */
20
- declare const PUBLIC_CANARY = "That reply wasn't safe to show. Try again.";
21
- /** Safe copy for tool or permission denials. */
22
- declare const PUBLIC_ACTION = "That action isn't available.";
23
- /** Safe copy for unsupported MIME types. */
24
- declare const PUBLIC_FILE_TYPE = "That file type isn't supported.";
25
- /** Safe copy for oversized files. */
26
- declare const PUBLIC_FILE_SIZE = "That file is too large.";
27
- /** Safe copy for too many files in one turn. */
28
- declare const PUBLIC_FILE_COUNT = "Too many files for one message.";
29
- /** Safe copy for unsupported generated image dimensions. */
30
- declare const PUBLIC_IMAGE_SIZE = "That image size isn't supported.";
31
- /** Safe copy when the host aborts a turn. */
32
- declare const PUBLIC_CANCELLED = "Cancelled.";
14
+ /** Snapshot of the registered default (ignores host overrides). Stable for tests. */
15
+ declare const PUBLIC_GENERIC: string;
16
+ declare const PUBLIC_UNAVAILABLE: string;
17
+ declare const PUBLIC_CANARY: string;
18
+ declare const PUBLIC_ACTION: string;
19
+ declare const PUBLIC_FILE_TYPE: string;
20
+ declare const PUBLIC_FILE_SIZE: string;
21
+ declare const PUBLIC_FILE_COUNT: string;
22
+ declare const PUBLIC_IMAGE_SIZE: string;
23
+ declare const PUBLIC_CANCELLED: string;
33
24
  /** True when `err` is an abort (DOMException or Error named AbortError). */
34
25
  declare function isAbortError(err: unknown): boolean;
35
26
  /** Throw if `signal` is already aborted. */
@@ -2,37 +2,26 @@
2
2
  * Public-safe error mapping for THEORUM.
3
3
  *
4
4
  * Kernel internals may contain provider status text, tool names, or exception
5
- * details. This module maps those failures to stable user-safe strings.
5
+ * details. This module maps those failures to stable user-safe strings from the
6
+ * kernel lexicon (`public.*` keys) so hosts can override them via
7
+ * `overrideLexicon`.
6
8
  *
7
9
  * @module
8
10
  */
9
- /** Error class used for expected THEORUM contract failures. */
10
- class TheorumError extends Error {
11
- constructor(message = '', options) {
12
- super(message, options);
13
- this.name = 'TheorumError';
14
- }
15
- }
11
+ import { lexiconDefault, lexiconText } from './lexicon.js';
12
+ import { TheorumError } from './theorum-error.js';
16
13
  /** Internal marker for provider or transport failure. */
17
14
  const UPSTREAM_FAILED = 'upstream failed';
18
- /** Generic safe fallback shown when details must not be surfaced. */
19
- const PUBLIC_GENERIC = 'Something went wrong. Try again.';
20
- /** Safe copy for transient provider unavailability. */
21
- const PUBLIC_UNAVAILABLE = 'The model is unavailable. Try again.';
22
- /** Safe copy for canary or egress disclosure violations. */
23
- const PUBLIC_CANARY = "That reply wasn't safe to show. Try again.";
24
- /** Safe copy for tool or permission denials. */
25
- const PUBLIC_ACTION = "That action isn't available.";
26
- /** Safe copy for unsupported MIME types. */
27
- const PUBLIC_FILE_TYPE = "That file type isn't supported.";
28
- /** Safe copy for oversized files. */
29
- const PUBLIC_FILE_SIZE = 'That file is too large.';
30
- /** Safe copy for too many files in one turn. */
31
- const PUBLIC_FILE_COUNT = 'Too many files for one message.';
32
- /** Safe copy for unsupported generated image dimensions. */
33
- const PUBLIC_IMAGE_SIZE = "That image size isn't supported.";
34
- /** Safe copy when the host aborts a turn. */
35
- const PUBLIC_CANCELLED = 'Cancelled.';
15
+ /** Snapshot of the registered default (ignores host overrides). Stable for tests. */
16
+ const PUBLIC_GENERIC = lexiconDefault('public.generic');
17
+ const PUBLIC_UNAVAILABLE = lexiconDefault('public.unavailable');
18
+ const PUBLIC_CANARY = lexiconDefault('public.canary');
19
+ const PUBLIC_ACTION = lexiconDefault('public.action');
20
+ const PUBLIC_FILE_TYPE = lexiconDefault('public.file_type');
21
+ const PUBLIC_FILE_SIZE = lexiconDefault('public.file_size');
22
+ const PUBLIC_FILE_COUNT = lexiconDefault('public.file_count');
23
+ const PUBLIC_IMAGE_SIZE = lexiconDefault('public.image_size');
24
+ const PUBLIC_CANCELLED = lexiconDefault('public.cancelled');
36
25
  /** True when `err` is an abort (DOMException or Error named AbortError). */
37
26
  function isAbortError(err) {
38
27
  if (!err || typeof err !== 'object') {
@@ -50,103 +39,123 @@ function throwIfAborted(signal) {
50
39
  if (isAbortError(reason)) {
51
40
  throw reason;
52
41
  }
53
- throw new DOMException('The operation was aborted.', 'AbortError');
42
+ throw new DOMException('The operation was aborted.', 'AbortError'); // lexicon-exempt: DOM AbortError fingerprint
43
+ }
44
+ /** Resolve public copy at call time so `overrideLexicon` takes effect. */
45
+ function publicCopy(key) {
46
+ return lexiconText(key);
54
47
  }
48
+ /** Upstream/internal message fingerprints → public lexicon keys (not emit copy). */
55
49
  const EXACT = {
56
- [UPSTREAM_FAILED]: PUBLIC_UNAVAILABLE,
57
- 'empty Gemini stream': PUBLIC_UNAVAILABLE,
58
- 'canary leaked': PUBLIC_CANARY,
59
- 'The operation was aborted.': PUBLIC_CANCELLED,
60
- 'This operation was aborted': PUBLIC_CANCELLED,
61
- 'Turn withheld: egress disclosure violation': PUBLIC_CANARY,
62
- 'expected JSON object': 'Something was wrong with that request.',
63
- 'structured output was not valid JSON': 'Something was wrong with that request.',
64
- 'malformed Gemini Live message': PUBLIC_UNAVAILABLE,
65
- 'malformed Gemini Live message during setup': PUBLIC_UNAVAILABLE,
66
- 'user input cannot be placed in the system block': PUBLIC_GENERIC,
67
- 'attachment data must be base64': PUBLIC_FILE_TYPE,
68
- 'attachment is too large': PUBLIC_FILE_SIZE,
69
- 'attachments exceed the per-turn budget': PUBLIC_FILE_SIZE,
70
- 'Tool input validation failed': "That question isn't valid.",
71
- 'This profile does not accept text input': PUBLIC_ACTION,
50
+ [UPSTREAM_FAILED]: 'public.unavailable', // lexicon-exempt: internal marker
51
+ 'empty Gemini stream': 'public.unavailable', // lexicon-exempt: upstream fingerprint
52
+ 'canary leaked': 'public.canary', // lexicon-exempt: internal marker
53
+ 'The operation was aborted.': 'public.cancelled', // lexicon-exempt: AbortError fingerprint
54
+ 'This operation was aborted': 'public.cancelled', // lexicon-exempt: AbortError fingerprint
55
+ 'Turn withheld: egress disclosure violation': 'public.canary', // lexicon-exempt: internal marker
56
+ 'expected JSON object': 'public.bad_request', // lexicon-exempt: upstream fingerprint
57
+ 'structured output was not valid JSON': 'public.bad_request', // lexicon-exempt: upstream fingerprint
58
+ 'malformed Gemini Live message': 'public.unavailable', // lexicon-exempt: upstream fingerprint
59
+ 'malformed Gemini Live message during setup': 'public.unavailable', // lexicon-exempt: upstream fingerprint
60
+ 'user input cannot be placed in the system block': 'public.generic', // lexicon-exempt: internal marker
61
+ 'attachment data must be base64': 'public.file_type', // lexicon-exempt: internal marker
62
+ 'attachment is too large': 'public.file_size', // lexicon-exempt: internal marker
63
+ 'attachments exceed the per-turn budget': 'public.file_size', // lexicon-exempt: internal marker
64
+ 'Tool input validation failed': 'public.invalid_question', // lexicon-exempt: internal marker
65
+ 'This profile does not accept text input': 'public.action', // lexicon-exempt: internal marker
72
66
  };
73
67
  const RULES = [
74
68
  {
75
69
  match: (t) => /^(Gemini|OpenRouter|TTS|OpenRouter TTS|Speech) HTTP/.test(t) ||
76
70
  t.includes('TTS HTTP') ||
77
71
  t.includes('Speech HTTP'),
78
- resolve: () => PUBLIC_UNAVAILABLE,
72
+ resolve: () => publicCopy('public.unavailable'),
79
73
  },
80
74
  {
81
- match: (t) => t.includes('not enabled on this turn') ||
75
+ match: (t) =>
76
+ // lexicon-exempt: substring fingerprints against internal TheorumError messages
77
+ t.includes('not enabled on this turn') ||
82
78
  t.includes('not allowed') ||
83
79
  t.includes('not registered') ||
84
- t.includes('Unknown model select') ||
85
- t.includes('Grounding tools'),
86
- resolve: () => PUBLIC_ACTION,
80
+ t.includes('Unknown model select') || // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
81
+ t.includes('Grounding tools') ||
82
+ (t.includes('live.ingress.') && t.includes('is disabled')),
83
+ resolve: () => publicCopy('public.action'),
87
84
  },
88
85
  {
89
- match: (t) => t.includes('MIME') ||
90
- t.includes('does not accept attachments') ||
91
- t.includes('does not accept voice'),
92
- resolve: () => PUBLIC_FILE_TYPE,
86
+ match: (t) =>
87
+ // lexicon-exempt: substring fingerprints against internal TheorumError messages
88
+ t.includes('MIME') ||
89
+ t.includes('does not accept attachments') || // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
90
+ t.includes('does not accept voice'), // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
91
+ resolve: () => publicCopy('public.file_type'),
93
92
  },
94
93
  {
95
94
  match: (t) => t.startsWith('At most'),
96
- resolve: () => PUBLIC_FILE_COUNT,
95
+ resolve: () => publicCopy('public.file_count'),
97
96
  },
98
97
  {
99
- match: (t) => (t.startsWith('Only ') && t.includes('file')) ||
100
- t.startsWith('Each file must be') ||
101
- t.startsWith('Those files together'),
98
+ match: (t) =>
99
+ // lexicon-exempt: match already-lexicon attachment copy before remapping
100
+ (t.startsWith('Only ') && t.includes('file')) ||
101
+ t.startsWith('Each file must be') || // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
102
+ t.startsWith('Those files together'), // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
102
103
  resolve: (t) => t,
103
104
  },
104
105
  {
105
106
  match: (t) => t.includes('attachment'),
106
- resolve: () => PUBLIC_FILE_SIZE,
107
+ resolve: () => publicCopy('public.file_size'),
107
108
  },
108
109
  {
110
+ // lexicon-exempt: substring fingerprint against internal TheorumError messages
109
111
  match: (t) => t.includes('aspect or size'),
110
- resolve: () => PUBLIC_IMAGE_SIZE,
112
+ resolve: () => publicCopy('public.image_size'),
111
113
  },
112
114
  {
113
- match: (t) => t.includes('must pin thinking') || t.includes('has no models'),
114
- resolve: () => PUBLIC_GENERIC,
115
+ match: (t) =>
116
+ // lexicon-exempt: substring fingerprints against internal TheorumError messages
117
+ t.includes('must pin thinking') || t.includes('has no models'),
118
+ resolve: () => publicCopy('public.generic'),
115
119
  },
116
120
  ];
117
- const ALREADY_PUBLIC = new Set([
118
- PUBLIC_GENERIC,
119
- PUBLIC_UNAVAILABLE,
120
- PUBLIC_CANARY,
121
- PUBLIC_ACTION,
122
- PUBLIC_FILE_TYPE,
123
- PUBLIC_FILE_SIZE,
124
- PUBLIC_FILE_COUNT,
125
- PUBLIC_IMAGE_SIZE,
126
- PUBLIC_CANCELLED,
127
- ]);
121
+ const PUBLIC_KEYS = [
122
+ 'public.generic',
123
+ 'public.unavailable',
124
+ 'public.canary',
125
+ 'public.action',
126
+ 'public.file_type',
127
+ 'public.file_size',
128
+ 'public.file_count',
129
+ 'public.image_size',
130
+ 'public.cancelled',
131
+ 'public.bad_request',
132
+ 'public.invalid_question',
133
+ ];
134
+ function isAlreadyPublic(text) {
135
+ return PUBLIC_KEYS.some((key) => lexiconText(key) === text || lexiconDefault(key) === text);
136
+ }
128
137
  function publicText(text) {
129
138
  if (/aborted/i.test(text)) {
130
- return PUBLIC_CANCELLED;
139
+ return publicCopy('public.cancelled');
131
140
  }
132
- if (ALREADY_PUBLIC.has(text)) {
141
+ if (isAlreadyPublic(text)) {
133
142
  return text;
134
143
  }
135
144
  const exact = EXACT[text];
136
145
  if (exact) {
137
- return exact;
146
+ return publicCopy(exact);
138
147
  }
139
148
  for (const rule of RULES) {
140
149
  if (rule.match(text)) {
141
150
  return rule.resolve(text);
142
151
  }
143
152
  }
144
- return PUBLIC_GENERIC;
153
+ return publicCopy('public.generic');
145
154
  }
146
155
  /** Convert an unknown thrown value or internal message to user-safe text. */
147
156
  function publicError(err) {
148
157
  if (isAbortError(err)) {
149
- return PUBLIC_CANCELLED;
158
+ return publicCopy('public.cancelled');
150
159
  }
151
160
  if (typeof err === 'string') {
152
161
  return publicText(err);
@@ -154,7 +163,7 @@ function publicError(err) {
154
163
  if (err instanceof TheorumError) {
155
164
  return publicText(err.message);
156
165
  }
157
- return PUBLIC_UNAVAILABLE;
166
+ return publicCopy('public.unavailable');
158
167
  }
159
168
  /** Raw diagnostic text for hosts, traces, and logs (never shown to end users). */
160
169
  function describeError(err) {
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Corpus acquisition for guardrail evaluation.
3
+ *
4
+ * Nothing is vendored. Corpora are fetched on demand and cached locally, so the
5
+ * published package carries no third-party data and no licence obligations beyond
6
+ * attribution here.
7
+ *
8
+ * Two sources, deliberately different in shape:
9
+ *
10
+ * - **S-Labs/prompt-injection-dataset** (MIT) — ~11k labelled prompts whose benign
11
+ * half deliberately includes security-adjacent questions ("explain output
12
+ * validation best practices", "how do I implement stress testing"). This is
13
+ * where user-text detectors are most likely to misfire.
14
+ *
15
+ * Chosen over `prodnull/prompt-injection-repo-dataset`, which has richer hard
16
+ * negatives but is gated: licence and access are separate axes, and a gated
17
+ * corpus cannot be fetched by an unattended run.
18
+ * - **AgentDojo** (MIT, ETH Zurich) — simulated environments for a tool-using
19
+ * agent. Its fixtures are read directly; the benchmark is never run, so no model
20
+ * or API key is involved. This supplies benign output in the shape a *tool*
21
+ * returns, which the repo dataset does not cover.
22
+ *
23
+ * The two are kept separate on purpose. Pooling sources and reporting one number
24
+ * hides the domain shift between them, and that shift is the thing most likely to
25
+ * make a detector look better than it is.
26
+ *
27
+ * @module
28
+ */
29
+ export interface CorpusSample {
30
+ text: string;
31
+ /** True when the sample is an attack. */
32
+ attack: boolean;
33
+ /** Source dataset id, kept so results are never pooled silently. */
34
+ source: string;
35
+ /** Benign sub-category, so "security docs" never averages into "work email". */
36
+ category: string;
37
+ }
38
+ export interface CorpusSource {
39
+ id: string;
40
+ licence: string;
41
+ attribution: string;
42
+ /**
43
+ * Rows fetched by default.
44
+ *
45
+ * Several of these corpora are far larger than a fast run wants. The cap is
46
+ * declared rather than buried in the loader so a report can say what fraction
47
+ * was actually sampled — a rate over 1% of a corpus is not a rate over the
48
+ * corpus.
49
+ */
50
+ sampleLimit: number;
51
+ /** Rows available upstream, when known, so sampling is visible in the report. */
52
+ upstreamRows?: number;
53
+ /** Set when the dataset is gated and needs `HF_TOKEN` to fetch. */
54
+ requiresToken?: boolean;
55
+ load: (cache: CorpusCache, limit: number) => Promise<CorpusSample[]>;
56
+ }
57
+ /** Local cache directory for fetched corpora; gitignored, never published. */
58
+ export interface CorpusCache {
59
+ dir: string;
60
+ fetchText: (url: string, key: string) => Promise<string>;
61
+ }
62
+ /**
63
+ * Page through a dataset via the rows API.
64
+ *
65
+ * Pages are fetched in batches rather than one at a time: a full corpus here runs
66
+ * to thousands of pages, and a serial walk is slow enough that it pressures whoever
67
+ * runs it into sampling a slice and quoting the result as if it covered the whole.
68
+ */
69
+ /** Paging options. An object rather than more positionals, which had reached five. */
70
+ export interface FetchRowsOptions {
71
+ split?: string;
72
+ config?: string;
73
+ /** Base retry delay. Seconds in production; tests pass milliseconds. */
74
+ retryBaseMs?: number;
75
+ }
76
+ declare function fetchRows(cache: CorpusCache, dataset: string, limit: number, options?: FetchRowsOptions): Promise<Record<string, unknown>[]>;
77
+ /** Create a cache that reads from disk when present and fetches when not. */
78
+ declare function createCorpusCache(dir: string): CorpusCache;
79
+ /**
80
+ * Parse a two-column `text,label` CSV.
81
+ *
82
+ * Hand-rolled because the corpus text is adversarial by construction: it contains
83
+ * quotes, commas, and embedded newlines, and a naive split would shred exactly the
84
+ * samples that matter most.
85
+ */
86
+ declare function parseLabelledCsv(input: string): {
87
+ text: string;
88
+ label: number;
89
+ }[];
90
+ declare function recordsFromYaml(yaml: string): string[];
91
+ /**
92
+ * Corpora evaluated and deliberately not enabled, with the reason.
93
+ *
94
+ * Kept in the repo so the search does not have to be repeated, and so a later
95
+ * decision to include one starts from the objection rather than from scratch.
96
+ */
97
+ /**
98
+ * Corpora evaluated and deliberately left out of SOURCES, with the objection.
99
+ * See docs/contracts/guardrails.md.
100
+ */
101
+ export declare const REVIEWED_SOURCES: readonly {
102
+ dataset: string;
103
+ rows: number;
104
+ licence: string;
105
+ verdict: string;
106
+ }[];
107
+ declare const SOURCES: readonly CorpusSource[];
108
+ export { createCorpusCache, fetchRows, parseLabelledCsv, recordsFromYaml, SOURCES };