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.
- package/README.md +81 -25
- package/esm/mod.d.ts +48 -29
- package/esm/mod.js +37 -24
- package/esm/src/cli/commands/bench.js +13 -16
- package/esm/src/cli/commands/fuzz-canary.js +25 -29
- package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
- package/esm/src/cli/commands/guardrails-eval.js +15 -0
- package/esm/src/cli/commands/profile.js +12 -3
- package/esm/src/cli/commands/run.js +8 -9
- package/esm/src/cli/commands/test.js +8 -8
- package/esm/src/cli/event-log.d.ts +1 -1
- package/esm/src/cli/index.js +12 -0
- package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
- package/esm/src/cli/matrix/synthesizer.js +23 -22
- package/esm/src/guardrails/canary-gate.d.ts +2 -1
- package/esm/src/guardrails/canary-gate.js +2 -1
- package/esm/src/guardrails/canary.d.ts +9 -1
- package/esm/src/guardrails/canary.js +21 -10
- package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
- package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
- package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
- package/esm/src/guardrails/corpus/live-attacks.js +0 -1
- package/esm/src/guardrails/corpus/mod.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.js +1 -0
- package/esm/src/guardrails/corpus/strings.d.ts +1 -0
- package/esm/src/guardrails/corpus/strings.js +1 -0
- package/esm/src/guardrails/corpus/types.d.ts +1 -0
- package/esm/src/guardrails/egress.d.ts +26 -3
- package/esm/src/guardrails/egress.js +69 -18
- package/esm/src/guardrails/error.d.ts +14 -23
- package/esm/src/guardrails/error.js +86 -77
- package/esm/src/guardrails/eval/corpus.d.ts +108 -0
- package/esm/src/guardrails/eval/corpus.js +978 -0
- package/esm/src/guardrails/eval/mod.d.ts +51 -0
- package/esm/src/guardrails/eval/mod.js +133 -0
- package/esm/src/guardrails/eval/score.d.ts +66 -0
- package/esm/src/guardrails/eval/score.js +114 -0
- package/esm/src/guardrails/events.d.ts +25 -0
- package/esm/src/guardrails/events.js +56 -0
- package/esm/src/guardrails/hits.d.ts +24 -0
- package/esm/src/guardrails/hits.js +45 -0
- package/esm/src/guardrails/lexicon.d.ts +39 -0
- package/esm/src/guardrails/lexicon.js +200 -0
- package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
- package/esm/src/guardrails/live-outbound-gate.js +131 -89
- package/esm/src/guardrails/mod.d.ts +19 -4
- package/esm/src/guardrails/mod.js +12 -3
- package/esm/src/guardrails/network.d.ts +19 -0
- package/esm/src/guardrails/network.js +234 -0
- package/esm/src/guardrails/policy.d.ts +35 -0
- package/esm/src/guardrails/policy.js +50 -0
- package/esm/src/guardrails/progressive-yield.d.ts +51 -0
- package/esm/src/guardrails/progressive-yield.js +98 -0
- package/esm/src/guardrails/quota.d.ts +17 -3
- package/esm/src/guardrails/quota.js +18 -4
- package/esm/src/guardrails/sanitize.d.ts +36 -6
- package/esm/src/guardrails/sanitize.js +152 -60
- package/esm/src/guardrails/serialize.d.ts +35 -0
- package/esm/src/guardrails/serialize.js +58 -0
- package/esm/src/guardrails/testing.d.ts +7 -0
- package/esm/src/guardrails/testing.js +4 -0
- package/esm/src/guardrails/theorum-error.d.ts +12 -0
- package/esm/src/guardrails/theorum-error.js +15 -0
- package/esm/src/guardrails/tool-directives.d.ts +48 -0
- package/esm/src/guardrails/tool-directives.js +124 -0
- package/esm/src/guardrails/tool-result.d.ts +93 -0
- package/esm/src/guardrails/tool-result.js +276 -0
- package/esm/src/guardrails/types.d.ts +291 -0
- package/esm/src/guardrails/types.js +72 -0
- package/esm/src/host/client-turn.js +4 -0
- package/esm/src/host/mint-trace.d.ts +1 -1
- package/esm/src/kernel/auth/crypto.d.ts +42 -0
- package/esm/src/kernel/auth/crypto.js +106 -0
- package/esm/src/kernel/auth/mod.d.ts +11 -0
- package/esm/src/kernel/auth/mod.js +11 -0
- package/esm/src/kernel/auth/oauth.d.ts +47 -0
- package/esm/src/kernel/auth/oauth.js +278 -0
- package/esm/src/kernel/auth/types.d.ts +133 -0
- package/esm/src/kernel/auth/types.js +13 -0
- package/esm/src/kernel/engine/delta.js +43 -2
- package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
- package/esm/src/kernel/engine/live-inbound.js +22 -8
- package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
- package/esm/src/kernel/engine/live-ingress.js +47 -0
- package/esm/src/kernel/engine/repair.js +13 -12
- package/esm/src/kernel/engine/runner/gates.js +127 -40
- package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
- package/esm/src/kernel/engine/runner/mod.js +182 -43
- package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
- package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
- package/esm/src/kernel/engine/runner/stages.js +89 -0
- package/esm/src/kernel/engine/runner/state.d.ts +24 -0
- package/esm/src/kernel/engine/runner/steps.js +128 -61
- package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
- package/esm/src/kernel/engine/runner/stream.js +114 -55
- package/esm/src/kernel/engine/session/mod.d.ts +3 -1
- package/esm/src/kernel/engine/session/mod.js +386 -34
- package/esm/src/kernel/interaction-parts.d.ts +14 -0
- package/esm/src/kernel/interaction-parts.js +23 -0
- package/esm/src/kernel/mod.d.ts +19 -7
- package/esm/src/kernel/mod.js +10 -5
- package/esm/src/kernel/profile-graph.d.ts +159 -0
- package/esm/src/kernel/profile-graph.js +156 -0
- package/esm/src/kernel/registry/attachments.d.ts +10 -10
- package/esm/src/kernel/registry/attachments.js +29 -26
- package/esm/src/kernel/registry/catalog.d.ts +25 -11
- package/esm/src/kernel/registry/catalog.js +59 -24
- package/esm/src/kernel/registry/ingress.js +34 -22
- package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
- package/esm/src/kernel/registry/profile-outputs.js +8 -0
- package/esm/src/kernel/registry/profiles.d.ts +34 -14
- package/esm/src/kernel/registry/profiles.js +306 -59
- package/esm/src/kernel/registry/provider-request.js +2 -0
- package/esm/src/kernel/registry/resolve.d.ts +7 -6
- package/esm/src/kernel/registry/resolve.js +102 -117
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/registry/sole-model.d.ts +8 -0
- package/esm/src/kernel/registry/sole-model.js +10 -0
- package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
- package/esm/src/kernel/registry/system-prompt.js +40 -0
- package/esm/src/kernel/registry/system-role.d.ts +8 -0
- package/esm/src/kernel/registry/system-role.js +14 -0
- package/esm/src/kernel/registry/vault.d.ts +2 -2
- package/esm/src/kernel/registry/vault.js +4 -4
- package/esm/src/kernel/schema.d.ts +97 -11
- package/esm/src/kernel/schema.js +256 -111
- package/esm/src/kernel/stages.d.ts +175 -0
- package/esm/src/kernel/stages.js +476 -0
- package/esm/src/kernel/stop.d.ts +65 -11
- package/esm/src/kernel/stop.js +46 -11
- package/esm/src/kernel/tools/events.d.ts +41 -0
- package/esm/src/kernel/tools/events.js +71 -0
- package/esm/src/kernel/tools/execute.d.ts +63 -24
- package/esm/src/kernel/tools/execute.js +475 -224
- package/esm/src/kernel/tools/harness.js +16 -14
- package/esm/src/kernel/tools/invoke.js +26 -8
- package/esm/src/kernel/tools/mod.d.ts +3 -1
- package/esm/src/kernel/tools/mod.js +2 -1
- package/esm/src/kernel/tools/permission.d.ts +15 -0
- package/esm/src/kernel/tools/permission.js +47 -0
- package/esm/src/kernel/tools/project.js +5 -3
- package/esm/src/kernel/tools/registry.js +20 -10
- package/esm/src/kernel/tools/remote.d.ts +94 -0
- package/esm/src/kernel/tools/remote.js +577 -0
- package/esm/src/kernel/tools/resolve.d.ts +17 -7
- package/esm/src/kernel/tools/resolve.js +38 -17
- package/esm/src/kernel/tools/schema.d.ts +2 -0
- package/esm/src/kernel/tools/schema.js +23 -6
- package/esm/src/kernel/tools/stage-run.d.ts +105 -0
- package/esm/src/kernel/tools/stage-run.js +155 -0
- package/esm/src/kernel/tools/types.d.ts +198 -20
- package/esm/src/kernel/types.d.ts +253 -114
- package/esm/src/kernel/util/find-last.d.ts +2 -0
- package/esm/src/kernel/util/find-last.js +10 -0
- package/esm/src/observability/destinations.d.ts +31 -0
- package/esm/src/observability/destinations.js +67 -0
- package/esm/src/observability/mod.d.ts +10 -3
- package/esm/src/observability/mod.js +6 -2
- package/esm/src/observability/policy.d.ts +27 -0
- package/esm/src/observability/policy.js +80 -0
- package/esm/src/observability/resolve-policy.d.ts +16 -0
- package/esm/src/observability/resolve-policy.js +64 -0
- package/esm/src/observability/trace-attach.d.ts +3 -1
- package/esm/src/observability/trace-attach.js +17 -10
- package/esm/src/observability/trace-record.d.ts +12 -4
- package/esm/src/observability/trace-record.js +63 -19
- package/esm/src/observability/trace-sink.d.ts +19 -0
- package/esm/src/observability/trace-sink.js +10 -0
- package/esm/src/observability/trace.d.ts +14 -11
- package/esm/src/observability/trace.js +23 -14
- package/esm/src/observability/types.d.ts +113 -0
- package/esm/src/observability/types.js +11 -0
- package/esm/src/providers/create-provider.d.ts +7 -4
- package/esm/src/providers/create-provider.js +21 -7
- package/esm/src/providers/google/interactions/framing.d.ts +8 -0
- package/esm/src/providers/google/interactions/framing.js +74 -13
- package/esm/src/providers/google/live/framing.d.ts +5 -2
- package/esm/src/providers/google/live/framing.js +49 -12
- package/esm/src/providers/google/live/openapi-schema.js +3 -2
- package/esm/src/providers/google/live/stream.d.ts +9 -0
- package/esm/src/providers/google/live/stream.js +16 -3
- package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
- package/esm/src/providers/openrouter/cache-control.js +23 -0
- package/esm/src/providers/openrouter/chat.d.ts +21 -1
- package/esm/src/providers/openrouter/chat.js +51 -6
- package/esm/src/providers/openrouter/image.js +8 -19
- package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
- package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
- package/esm/src/providers/openrouter/openai/compat.js +23 -4
- package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
- package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
- package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
- package/package.json +1 -1
- package/esm/src/interface/blocks.d.ts +0 -20
- package/esm/src/interface/blocks.js +0 -180
- package/esm/src/interface/from-profile.d.ts +0 -14
- package/esm/src/interface/from-profile.js +0 -104
- package/esm/src/interface/inputs.d.ts +0 -8
- package/esm/src/interface/inputs.js +0 -127
- package/esm/src/interface/mod.d.ts +0 -10
- package/esm/src/interface/mod.js +0 -9
- package/esm/src/interface/types.d.ts +0 -153
- 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
|
-
/**
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
59
|
-
rejection:
|
|
60
|
-
guidance
|
|
83
|
+
previousOutput: previous.text,
|
|
84
|
+
rejection: rejection.text,
|
|
85
|
+
...(guidance ? { guidance } : {}),
|
|
61
86
|
};
|
|
62
87
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
/**
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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,
|
|
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 };
|