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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Guardrail evaluation — measured detector quality, not asserted.
3
+ *
4
+ * Every detector in this facet is a pattern matcher, and pattern matchers fail on
5
+ * content nobody thought to write down. The point of this module is to make that
6
+ * failure visible against corpora the authors did not choose, rather than against
7
+ * hand-picked examples that flatter whatever was just built.
8
+ *
9
+ * Reached from `theorum/guardrails/testing`; never from the production entry.
10
+ *
11
+ * @command `deno task guardrails:eval`
12
+ * @module
13
+ */
14
+ import { type DetectorScore, type EvalDetector } from './score.js';
15
+ /**
16
+ * The detectors under evaluation, with what they do when they fire.
17
+ *
18
+ * `action` is not decoration: it sets how much a false positive costs. A `redact`
19
+ * silently rewrites a user's message, a `block` withholds a whole turn, and an
20
+ * `annotate` only adds a caution the model may disregard. The first two have to
21
+ * clear a far higher bar.
22
+ */
23
+ declare const DETECTORS: readonly EvalDetector[];
24
+ export interface EvalOptions {
25
+ /** Cache directory for fetched corpora. Gitignored; never published. */
26
+ cacheDir?: string;
27
+ /** Cap samples per source, for a quick local run. */
28
+ limit?: number;
29
+ }
30
+ export interface EvalReport {
31
+ scores: DetectorScore[];
32
+ /** Attribution for every corpus actually loaded. */
33
+ sources: {
34
+ id: string;
35
+ licence: string;
36
+ attribution: string;
37
+ samples: number;
38
+ /** Rows available upstream, so partial sampling is visible. */
39
+ upstreamRows?: number;
40
+ }[];
41
+ /** Corpora that could not be loaded, and why. */
42
+ skipped: {
43
+ id: string;
44
+ reason: string;
45
+ }[];
46
+ }
47
+ /** Fetch the corpora and score every detector against each source separately. */
48
+ declare function runGuardrailEval(options?: EvalOptions): Promise<EvalReport>;
49
+ /** Human-readable report, including the attribution the licences require. */
50
+ declare function formatReport(report: EvalReport): string;
51
+ export { DETECTORS, formatReport, runGuardrailEval };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Guardrail evaluation — measured detector quality, not asserted.
3
+ *
4
+ * Every detector in this facet is a pattern matcher, and pattern matchers fail on
5
+ * content nobody thought to write down. The point of this module is to make that
6
+ * failure visible against corpora the authors did not choose, rather than against
7
+ * hand-picked examples that flatter whatever was just built.
8
+ *
9
+ * Reached from `theorum/guardrails/testing`; never from the production entry.
10
+ *
11
+ * @command `deno task guardrails:eval`
12
+ * @module
13
+ */
14
+ /** lexicon-exempt-file: evaluation runner — not runtime user or model copy (P2) */
15
+ import { injectionSpans } from '../injection.js';
16
+ import { sensitiveSpans } from '../sensitive.js';
17
+ import { directiveHits } from '../tool-directives.js';
18
+ import { createCorpusCache, SOURCES } from './corpus.js';
19
+ import { formatScores, scoreAll } from './score.js';
20
+ /** Tool names used for the callable-tool signal when scoring tool-result content. */
21
+ const EVAL_TOOLS = ['send_email', 'send_money', 'read_inbox', 'get_channel_messages'];
22
+ /**
23
+ * The detectors under evaluation, with what they do when they fire.
24
+ *
25
+ * `action` is not decoration: it sets how much a false positive costs. A `redact`
26
+ * silently rewrites a user's message, a `block` withholds a whole turn, and an
27
+ * `annotate` only adds a caution the model may disregard. The first two have to
28
+ * clear a far higher bar.
29
+ */
30
+ const DETECTORS = [
31
+ {
32
+ id: 'injection.spans',
33
+ action: 'redact',
34
+ accountableFor: [
35
+ 'prompt-injection-prompts',
36
+ 'deepset-prompts',
37
+ 'spml-chatbot',
38
+ 'agent-app-attacks',
39
+ 'nvidia-agentic-ipi',
40
+ 'llmail-adaptive',
41
+ 'llmail-evaded-defense',
42
+ 'multilingual-prompts',
43
+ 'nvidia-jailbreak',
44
+ 'wildjailbreak',
45
+ ],
46
+ fires: (text) => injectionSpans(text).length > 0,
47
+ },
48
+ {
49
+ id: 'sensitive.spans',
50
+ action: 'redact',
51
+ // `pii-spans` is the one corpus that labels what this detector hunts.
52
+ accountableFor: ['pii-spans'],
53
+ fires: (text) => sensitiveSpans(text).length > 0,
54
+ },
55
+ {
56
+ id: 'tool-directives',
57
+ // Indirect injection inside tool output. The prompt corpora are user-text
58
+ // shaped; the agent-app attacks are the closest available match.
59
+ action: 'annotate',
60
+ accountableFor: [
61
+ 'agent-app-attacks',
62
+ 'nvidia-agentic-ipi',
63
+ 'llmail-adaptive',
64
+ 'llmail-evaded-defense',
65
+ ],
66
+ fires: (text) => directiveHits(text, EVAL_TOOLS).length > 0,
67
+ },
68
+ ];
69
+ function corpusSkipReason(source, kind, err) {
70
+ if (kind === 'error') {
71
+ if (source.requiresToken)
72
+ return 'gated upstream — set HF_TOKEN to include it';
73
+ return err instanceof Error ? err.message : String(err);
74
+ }
75
+ return source.requiresToken
76
+ ? 'no rows returned — gated, or upstream rate-limited'
77
+ : 'no rows returned — upstream rate-limited or schema changed';
78
+ }
79
+ async function loadEvalSource(source, cache, limit) {
80
+ let samples;
81
+ try {
82
+ samples = await source.load(cache, limit ?? source.sampleLimit);
83
+ }
84
+ catch (err) {
85
+ return { ok: false, skip: { id: source.id, reason: corpusSkipReason(source, 'error', err) } };
86
+ }
87
+ if (samples.length === 0 && (source.upstreamRows ?? 0) > 0) {
88
+ return { ok: false, skip: { id: source.id, reason: corpusSkipReason(source, 'empty') } };
89
+ }
90
+ return {
91
+ ok: true,
92
+ samples,
93
+ meta: {
94
+ id: source.id,
95
+ licence: source.licence,
96
+ attribution: source.attribution,
97
+ samples: samples.length,
98
+ ...(source.upstreamRows !== undefined ? { upstreamRows: source.upstreamRows } : {}),
99
+ },
100
+ };
101
+ }
102
+ /** Fetch the corpora and score every detector against each source separately. */
103
+ async function runGuardrailEval(options = {}) {
104
+ const cache = createCorpusCache(options.cacheDir ?? '.guardrail-corpus');
105
+ const bySource = new Map();
106
+ const sources = [];
107
+ const skipped = [];
108
+ for (const source of SOURCES) {
109
+ const loaded = await loadEvalSource(source, cache, options.limit);
110
+ if (!loaded.ok) {
111
+ skipped.push(loaded.skip);
112
+ continue;
113
+ }
114
+ bySource.set(source.id, loaded.samples);
115
+ sources.push(loaded.meta);
116
+ }
117
+ return { scores: scoreAll(DETECTORS, bySource), sources, skipped };
118
+ }
119
+ /** Human-readable report, including the attribution the licences require. */
120
+ function formatReport(report) {
121
+ const header = report.sources
122
+ .map((s) => {
123
+ // Say plainly when a figure rests on a slice of a much larger corpus.
124
+ const of = s.upstreamRows !== undefined && s.samples < s.upstreamRows ? ` of ${s.upstreamRows}` : '';
125
+ return ` ${s.id.padEnd(24)} ${String(s.samples).padStart(6)}${of.padEnd(12)} samples ${s.licence} ${s.attribution}`;
126
+ })
127
+ .join('\n');
128
+ const skipped = report.skipped.length === 0
129
+ ? ''
130
+ : `\n\nNot loaded\n${report.skipped.map((s) => ` ${s.id.padEnd(24)} ${s.reason}`).join('\n')}`;
131
+ return `Corpora\n${header}${skipped}\n${formatScores(report.scores)}`;
132
+ }
133
+ export { DETECTORS, formatReport, runGuardrailEval };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Scoring for guardrail evaluation.
3
+ *
4
+ * Reports per detector, per source, and per benign category. Never a single
5
+ * pooled number: pooling hides which kind of content a detector misfires on, and
6
+ * "security documentation" and "work email" fail for different reasons.
7
+ *
8
+ * The false-positive rate is the number that decides whether a detector ships. A
9
+ * detector that fires on legitimate content does not make an agent careful; it
10
+ * makes it unreliable in ways a user cannot see the reason for.
11
+ *
12
+ * @module
13
+ */
14
+ import type { CorpusSample } from './corpus.js';
15
+ /** One detector under evaluation. */
16
+ export interface EvalDetector {
17
+ id: string;
18
+ /** What the detector does when it fires — determines how costly a miss is. */
19
+ action: 'redact' | 'block' | 'annotate';
20
+ /**
21
+ * Sources whose attack label this detector is answerable for.
22
+ *
23
+ * A credential detector scored against a corpus of prompt injections would
24
+ * report near-zero recall and look broken, when in fact it was asked the wrong
25
+ * question. Recall is reported only where the corpus labels the thing the
26
+ * detector exists to find; false-positive rate is always reported, because
27
+ * benign is benign whatever the detector is looking for.
28
+ */
29
+ accountableFor: readonly string[];
30
+ fires: (text: string) => boolean;
31
+ }
32
+ export interface CategoryScore {
33
+ category: string;
34
+ samples: number;
35
+ fired: number;
36
+ /** Rate at which the detector fired on this category. */
37
+ rate: number;
38
+ }
39
+ export interface DetectorScore {
40
+ detector: string;
41
+ action: EvalDetector['action'];
42
+ source: string;
43
+ attacks: number;
44
+ attacksCaught: number;
45
+ /** Undefined when the source carries no attacks. */
46
+ recall?: number;
47
+ benign: number;
48
+ falsePositives: number;
49
+ /** Undefined when the source carries no benign samples. */
50
+ falsePositiveRate?: number;
51
+ /** Benign breakdown, so one bad category cannot hide inside an average. */
52
+ byCategory: CategoryScore[];
53
+ }
54
+ /** Score one detector against one source's samples. */
55
+ declare function scoreDetector(detector: EvalDetector, source: string, samples: readonly CorpusSample[]): DetectorScore;
56
+ /**
57
+ * Score every detector against every source, keeping sources apart.
58
+ *
59
+ * Sources are never merged. A detector tuned on one corpus routinely collapses on
60
+ * another, and a combined figure would report the average of a good result and a
61
+ * bad one as though it were a single fact.
62
+ */
63
+ declare function scoreAll(detectors: readonly EvalDetector[], bySource: ReadonlyMap<string, readonly CorpusSample[]>): DetectorScore[];
64
+ /** Render scores as a report, marking any figure too small to be a claim. */
65
+ declare function formatScores(scores: readonly DetectorScore[]): string;
66
+ export { formatScores, scoreAll, scoreDetector };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Scoring for guardrail evaluation.
3
+ *
4
+ * Reports per detector, per source, and per benign category. Never a single
5
+ * pooled number: pooling hides which kind of content a detector misfires on, and
6
+ * "security documentation" and "work email" fail for different reasons.
7
+ *
8
+ * The false-positive rate is the number that decides whether a detector ships. A
9
+ * detector that fires on legitimate content does not make an agent careful; it
10
+ * makes it unreliable in ways a user cannot see the reason for.
11
+ *
12
+ * @module
13
+ */
14
+ function rate(n, of) {
15
+ return of === 0 ? undefined : n / of;
16
+ }
17
+ /** Score one detector against one source's samples. */
18
+ function scoreDetector(detector, source, samples) {
19
+ const attacks = samples.filter((s) => s.attack);
20
+ const benign = samples.filter((s) => !s.attack);
21
+ const accountable = detector.accountableFor.includes(source);
22
+ const caught = attacks.filter((s) => detector.fires(s.text)).length;
23
+ const falsePositives = benign.filter((s) => detector.fires(s.text)).length;
24
+ const categories = new Map();
25
+ for (const sample of benign) {
26
+ const entry = categories.get(sample.category) ?? { samples: 0, fired: 0 };
27
+ entry.samples++;
28
+ if (detector.fires(sample.text)) {
29
+ entry.fired++;
30
+ }
31
+ categories.set(sample.category, entry);
32
+ }
33
+ return {
34
+ detector: detector.id,
35
+ action: detector.action,
36
+ source,
37
+ attacks: accountable ? attacks.length : 0,
38
+ attacksCaught: accountable ? caught : 0,
39
+ recall: accountable ? rate(caught, attacks.length) : undefined,
40
+ benign: benign.length,
41
+ falsePositives,
42
+ falsePositiveRate: rate(falsePositives, benign.length),
43
+ byCategory: [...categories.entries()]
44
+ .map(([category, entry]) => ({
45
+ category,
46
+ samples: entry.samples,
47
+ fired: entry.fired,
48
+ rate: entry.samples === 0 ? 0 : entry.fired / entry.samples,
49
+ }))
50
+ .sort((a, b) => b.rate - a.rate),
51
+ };
52
+ }
53
+ /**
54
+ * Score every detector against every source, keeping sources apart.
55
+ *
56
+ * Sources are never merged. A detector tuned on one corpus routinely collapses on
57
+ * another, and a combined figure would report the average of a good result and a
58
+ * bad one as though it were a single fact.
59
+ */
60
+ function scoreAll(detectors, bySource) {
61
+ const out = [];
62
+ for (const detector of detectors) {
63
+ for (const [source, samples] of bySource) {
64
+ if (samples.length > 0) {
65
+ out.push(scoreDetector(detector, source, samples));
66
+ }
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function pct(value) {
72
+ return value === undefined ? ' — ' : `${(value * 100).toFixed(1).padStart(5)}%`;
73
+ }
74
+ /** Sample count below which a rate is noise rather than a measurement. */
75
+ const MEANINGFUL_N = 200;
76
+ function groupScoresByDetector(scores) {
77
+ const byDetector = new Map();
78
+ for (const score of scores) {
79
+ byDetector.set(score.detector, [...(byDetector.get(score.detector) ?? []), score]);
80
+ }
81
+ return byDetector;
82
+ }
83
+ function formatRecall(score) {
84
+ if (score.recall === undefined) {
85
+ return 'recall n/a ';
86
+ }
87
+ return `recall ${pct(score.recall)} (${score.attacksCaught}/${score.attacks})`;
88
+ }
89
+ function formatSourceLine(score) {
90
+ const thin = score.benign > 0 && score.benign < MEANINGFUL_N ? ' [n too small]' : '';
91
+ return (` ${score.source.padEnd(24)} ${formatRecall(score)}` +
92
+ ` false+ ${pct(score.falsePositiveRate)} (${score.falsePositives}/${score.benign})${thin}`);
93
+ }
94
+ function formatFiredCategories(score) {
95
+ const lines = [];
96
+ for (const category of score.byCategory) {
97
+ if (category.fired <= 0)
98
+ continue;
99
+ lines.push(` ${category.category.padEnd(22)} ${pct(category.rate)} (${category.fired}/${category.samples})`);
100
+ }
101
+ return lines;
102
+ }
103
+ /** Render scores as a report, marking any figure too small to be a claim. */
104
+ function formatScores(scores) {
105
+ const lines = [];
106
+ for (const [detector, group] of groupScoresByDetector(scores)) {
107
+ lines.push(`\n${detector} (on fire: ${group[0]?.action ?? 'unknown'})`);
108
+ for (const score of group) {
109
+ lines.push(formatSourceLine(score), ...formatFiredCategories(score));
110
+ }
111
+ }
112
+ return lines.join('\n');
113
+ }
114
+ export { formatScores, scoreAll, scoreDetector };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Turn-event constructors for guardrail decisions.
3
+ *
4
+ * Keeps `{ type: 'guardrail', guardrail }` shaping in one place so sanitize,
5
+ * egress, tools, network, and live paths cannot drift.
6
+ *
7
+ * @module
8
+ */
9
+ import type { TurnEvent } from '../kernel/types.js';
10
+ import type { GuardrailAction, GuardrailEvent, GuardrailHit, GuardrailStage, Provenance, TrustLevel, Verdict } from './types.js';
11
+ /** Wrap a GuardrailEvent as a turn stream event. */
12
+ declare function guardrailTurnEvent(guardrail: GuardrailEvent): TurnEvent;
13
+ /**
14
+ * Build a turn event from a Verdict. `allow` yields nothing (clean = silence).
15
+ * `flag` / `redact` / `block` all emit so hosts and traces can count hits.
16
+ */
17
+ declare function guardrailFromVerdict(stage: GuardrailStage, trust: TrustLevel, verdict: Verdict, provenance?: Provenance): TurnEvent | undefined;
18
+ /** Build a turn event from a hit list. Empty hits → undefined. */
19
+ declare function guardrailFromHits(stage: GuardrailStage, trust: TrustLevel, hits: GuardrailHit[], action?: GuardrailAction, provenance?: Provenance): TurnEvent | undefined;
20
+ /**
21
+ * Project a guardrail turn event for host/trace: strip `hit.match` unless opted in.
22
+ * Non-guardrail events pass through unchanged.
23
+ */
24
+ declare function projectGuardrailTurnEvent(event: TurnEvent, includeMatch: boolean): TurnEvent;
25
+ export { guardrailFromHits, guardrailFromVerdict, guardrailTurnEvent, projectGuardrailTurnEvent };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Turn-event constructors for guardrail decisions.
3
+ *
4
+ * Keeps `{ type: 'guardrail', guardrail }` shaping in one place so sanitize,
5
+ * egress, tools, network, and live paths cannot drift.
6
+ *
7
+ * @module
8
+ */
9
+ import { projectGuardrailEvent } from './hits.js';
10
+ /** Wrap a GuardrailEvent as a turn stream event. */
11
+ function guardrailTurnEvent(guardrail) {
12
+ return { type: 'guardrail', guardrail };
13
+ }
14
+ /**
15
+ * Build a turn event from a Verdict. `allow` yields nothing (clean = silence).
16
+ * `flag` / `redact` / `block` all emit so hosts and traces can count hits.
17
+ */
18
+ function guardrailFromVerdict(stage, trust, verdict, provenance) {
19
+ if (verdict.action === 'allow') {
20
+ return undefined;
21
+ }
22
+ return guardrailTurnEvent({
23
+ stage,
24
+ trust,
25
+ action: verdict.action,
26
+ hits: verdict.hits,
27
+ ...(provenance ? { provenance } : {}),
28
+ });
29
+ }
30
+ /** Build a turn event from a hit list. Empty hits → undefined. */
31
+ function guardrailFromHits(stage, trust, hits, action = 'redact', provenance) {
32
+ if (hits.length === 0) {
33
+ return undefined;
34
+ }
35
+ return guardrailTurnEvent({
36
+ stage,
37
+ trust,
38
+ action,
39
+ hits,
40
+ ...(provenance ? { provenance } : {}),
41
+ });
42
+ }
43
+ /**
44
+ * Project a guardrail turn event for host/trace: strip `hit.match` unless opted in.
45
+ * Non-guardrail events pass through unchanged.
46
+ */
47
+ function projectGuardrailTurnEvent(event, includeMatch) {
48
+ if (event.type !== 'guardrail' || !event.guardrail) {
49
+ return event;
50
+ }
51
+ return {
52
+ ...event,
53
+ guardrail: projectGuardrailEvent(event.guardrail, includeMatch),
54
+ };
55
+ }
56
+ export { guardrailFromHits, guardrailFromVerdict, guardrailTurnEvent, projectGuardrailTurnEvent };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Guardrail hit helpers — match previews and host/trace projection.
3
+ *
4
+ * Detectors may attach `match` (the exact substring). Projection strips it unless
5
+ * the host opted into `observability.include.guardrailMatchPreview`.
6
+ *
7
+ * @module
8
+ */
9
+ import type { GuardrailEvent, GuardrailHit, Severity } from './types.js';
10
+ /** Cap for `GuardrailHit.match` so PEM / long blobs do not explode logs. */
11
+ declare const GUARDRAIL_MATCH_PREVIEW_MAX = 512;
12
+ /** Slice + cap the matched substring for debugging. */
13
+ declare function matchPreview(text: string, start: number, end: number): string;
14
+ /** Build a span hit with an optional match preview from the inspected text. */
15
+ declare function hitFromSpan(text: string, span: {
16
+ start: number;
17
+ end: number;
18
+ }, rule: string, severity: Severity): GuardrailHit;
19
+ /**
20
+ * Strip or keep `match` on hits.
21
+ * Default host/trace posture is strip — opt in via profile observability.
22
+ */
23
+ declare function projectGuardrailEvent(event: GuardrailEvent, includeMatch: boolean): GuardrailEvent;
24
+ export { GUARDRAIL_MATCH_PREVIEW_MAX, hitFromSpan, matchPreview, projectGuardrailEvent };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Guardrail hit helpers — match previews and host/trace projection.
3
+ *
4
+ * Detectors may attach `match` (the exact substring). Projection strips it unless
5
+ * the host opted into `observability.include.guardrailMatchPreview`.
6
+ *
7
+ * @module
8
+ */
9
+ /** Cap for `GuardrailHit.match` so PEM / long blobs do not explode logs. */
10
+ const GUARDRAIL_MATCH_PREVIEW_MAX = 512;
11
+ /** Slice + cap the matched substring for debugging. */
12
+ function matchPreview(text, start, end) {
13
+ const raw = text.slice(start, end);
14
+ if (raw.length <= GUARDRAIL_MATCH_PREVIEW_MAX) {
15
+ return raw;
16
+ }
17
+ return `${raw.slice(0, GUARDRAIL_MATCH_PREVIEW_MAX)}…`;
18
+ }
19
+ /** Build a span hit with an optional match preview from the inspected text. */
20
+ function hitFromSpan(text, span, rule, severity) {
21
+ return {
22
+ rule,
23
+ severity,
24
+ span: { start: span.start, end: span.end },
25
+ match: matchPreview(text, span.start, span.end),
26
+ };
27
+ }
28
+ /**
29
+ * Strip or keep `match` on hits.
30
+ * Default host/trace posture is strip — opt in via profile observability.
31
+ */
32
+ function projectGuardrailEvent(event, includeMatch) {
33
+ if (includeMatch) {
34
+ return event;
35
+ }
36
+ return {
37
+ ...event,
38
+ hits: event.hits.map(({ rule, severity, span }) => ({
39
+ rule,
40
+ severity,
41
+ ...(span ? { span } : {}),
42
+ })),
43
+ };
44
+ }
45
+ export { GUARDRAIL_MATCH_PREVIEW_MAX, hitFromSpan, matchPreview, projectGuardrailEvent };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Kernel lexicon — the registered defaults for every English string the kernel
3
+ * may emit toward a user or a model.
4
+ *
5
+ * "Host decides, Theorum runs": the kernel may ship overridable defaults for
6
+ * mechanism text, never unreplaceable copy. Every kernel emit-site imports its
7
+ * string from here, so a host can replace all of them in one place
8
+ * (`overrideLexicon`), and the copy-manifest lint
9
+ * (`scripts/docs-truth/copy-lint.mjs`) fails the build when prose appears
10
+ * anywhere else in `src/kernel`, `src/guardrails`, or `src/interface`.
11
+ *
12
+ * Imports `TheorumError` from `./theorum-error.ts` (not `./error.ts`) to avoid
13
+ * a cycle — `error.ts` resolves public-safe copy through this module.
14
+ *
15
+ * @module
16
+ */
17
+ /** Substitution parameters for a lexicon template. */
18
+ export type LexiconParams = Record<string, string | number>;
19
+ /** Registered default strings the kernel can emit. Keys are stable API. */
20
+ export declare const LEXICON_KEYS: readonly ["continue.instruction", "canary.bind_note", "taint.blocked", "taint.reason_steered", "taint.reason_tainted", "advisory.notice_elevated", "advisory.notice_high", "attachments.too_many_files", "attachments.file_too_large", "attachments.turn_too_large", "attachments.not_accepted", "attachments.mime_not_allowed", "attachments.limits_unconfigured", "public.generic", "public.unavailable", "public.canary", "public.action", "public.file_type", "public.file_size", "public.file_count", "public.image_size", "public.cancelled", "public.bad_request", "public.invalid_question", "repair.default_guidance", "repair.prompt_header", "repair.prompt_intro", "repair.prompt_instructions", "repair.history_heading", "repair.section_previous_output", "repair.section_validator_rejection", "repair.section_repair_guidance", "repair.section_instructions", "egress.default_repair_guidance", "session.abandon_gated", "session.tool_denied", "tool.awaiting_user", "tool.t2_loader_needs_snapshot", "tool.t2_loader_shape", "tool.t2_loader_output_invalid", "tool.input_invalid", "tool.input_invalid_after_mutate", "tool.output_invalid_after_mutate", "tool.handler_no_output", "tool.output_invalid", "tool.not_wired_t1", "tool.not_loaded_t2", "tool.not_visible", "tool.builtin_not_enabled", "tool.provider_native", "tool.not_registered", "tool.builtin_needs_snapshot", "tool.not_allowed", "tool.not_eligible", "tool.unsupported_type"];
21
+ export type LexiconKey = (typeof LEXICON_KEYS)[number];
22
+ /** Host-supplied replacement templates, `{param}` placeholders included. */
23
+ export type LexiconOverrides = Partial<Record<LexiconKey, string>>;
24
+ /**
25
+ * Replace registered defaults with host copy. Follows the
26
+ * `registerTraceDestination` pattern: process-level registration owned by the
27
+ * host. Templates keep `{param}` placeholders; mechanism-critical placeholders
28
+ * (the canary token) are validated here.
29
+ */
30
+ export declare function overrideLexicon(entries: LexiconOverrides): void;
31
+ /** Drop all host overrides (tests / host teardown). */
32
+ export declare function resetLexicon(): void;
33
+ /**
34
+ * Resolve one string: explicit host template (per call site, e.g. a profile
35
+ * field) → process override → registered default.
36
+ */
37
+ export declare function lexiconText(key: LexiconKey, params?: LexiconParams, hostTemplate?: string): string;
38
+ /** The registered default for a key, rendered with `params`. Ignores overrides. */
39
+ export declare function lexiconDefault(key: LexiconKey, params?: LexiconParams): string;