@agent-inspect/eval 2.4.0 → 2.6.0
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/dist/index.cjs +936 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.mjs +931 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,920 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var readers = require('agent-inspect/readers');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
9
|
+
|
|
10
|
+
// packages/eval/src/index.ts
|
|
11
|
+
var ALL_RULES = [
|
|
12
|
+
"circuit.same-tool-repetition",
|
|
13
|
+
"circuit.same-args-repetition",
|
|
14
|
+
"circuit.max-loop-iterations",
|
|
15
|
+
"circuit.max-retries",
|
|
16
|
+
"circuit.tool-timeout",
|
|
17
|
+
"circuit.runaway-llm-loop",
|
|
18
|
+
"circuit.excessive-branch-width"
|
|
19
|
+
];
|
|
20
|
+
function closed(ruleId, message) {
|
|
21
|
+
return { ruleId, status: "closed", severity: "info", message, evidence: [] };
|
|
22
|
+
}
|
|
23
|
+
function open(ruleId, message, evidence, severity = "error") {
|
|
24
|
+
return {
|
|
25
|
+
ruleId,
|
|
26
|
+
status: severity === "warning" ? "warn" : "open",
|
|
27
|
+
severity,
|
|
28
|
+
message,
|
|
29
|
+
evidence
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isToolEvent(event) {
|
|
33
|
+
const name = event.name.toLowerCase();
|
|
34
|
+
return event.kind === "tool" || name.startsWith("tool:") || name.startsWith("function:") || name.includes(".tool.") || name.startsWith("mcp:");
|
|
35
|
+
}
|
|
36
|
+
function isLlmEvent(event) {
|
|
37
|
+
const name = event.name.toLowerCase();
|
|
38
|
+
return event.kind === "llm" || name.startsWith("llm:") || name.includes(".llm.") || name.includes("generation");
|
|
39
|
+
}
|
|
40
|
+
function toolLabel(event) {
|
|
41
|
+
const attrs = event.attributes ?? {};
|
|
42
|
+
const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;
|
|
43
|
+
if (typeof fromAttr === "string" && fromAttr.length > 0) return fromAttr;
|
|
44
|
+
return event.name.replace(/^(tool:|function:|mcp:)/i, "");
|
|
45
|
+
}
|
|
46
|
+
function argsHash(toolName, args) {
|
|
47
|
+
return crypto.createHash("sha256").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest("hex").slice(0, 16);
|
|
48
|
+
}
|
|
49
|
+
function toolArgs(event) {
|
|
50
|
+
const attrs = event.attributes ?? {};
|
|
51
|
+
return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;
|
|
52
|
+
}
|
|
53
|
+
function durationMs(event) {
|
|
54
|
+
if (typeof event.durationMs === "number") return event.durationMs;
|
|
55
|
+
const attrs = event.attributes ?? {};
|
|
56
|
+
const fromAttr = attrs.durationMs ?? attrs.duration;
|
|
57
|
+
return typeof fromAttr === "number" ? fromAttr : void 0;
|
|
58
|
+
}
|
|
59
|
+
function attemptNumber(event) {
|
|
60
|
+
const attrs = event.attributes ?? {};
|
|
61
|
+
const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;
|
|
62
|
+
return typeof value === "number" ? value : void 0;
|
|
63
|
+
}
|
|
64
|
+
function evaluateSameToolRepetition(events, maxRepeats) {
|
|
65
|
+
const ruleId = "circuit.same-tool-repetition";
|
|
66
|
+
const counts = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const event of events.filter(isToolEvent)) {
|
|
68
|
+
const label = toolLabel(event);
|
|
69
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const evidence = [];
|
|
72
|
+
for (const [toolName, count] of counts) {
|
|
73
|
+
if (count > maxRepeats) {
|
|
74
|
+
evidence.push({ ruleId, toolName, count, threshold: maxRepeats });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (evidence.length === 0) {
|
|
78
|
+
return closed(ruleId, "Tool repetition within threshold.");
|
|
79
|
+
}
|
|
80
|
+
return open(ruleId, "Same tool repeated beyond threshold.", evidence);
|
|
81
|
+
}
|
|
82
|
+
function evaluateSameArgsRepetition(events, maxRepeats) {
|
|
83
|
+
const ruleId = "circuit.same-args-repetition";
|
|
84
|
+
const counts = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const event of events.filter(isToolEvent)) {
|
|
86
|
+
const label = toolLabel(event);
|
|
87
|
+
const hash = argsHash(label, toolArgs(event));
|
|
88
|
+
const key = `${label}:${hash}`;
|
|
89
|
+
const current = counts.get(key) ?? { toolName: label, count: 0 };
|
|
90
|
+
current.count += 1;
|
|
91
|
+
counts.set(key, current);
|
|
92
|
+
}
|
|
93
|
+
const evidence = [];
|
|
94
|
+
for (const entry of counts.values()) {
|
|
95
|
+
if (entry.count > maxRepeats) {
|
|
96
|
+
evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (evidence.length === 0) {
|
|
100
|
+
return closed(ruleId, "Tool argument repetition within threshold.");
|
|
101
|
+
}
|
|
102
|
+
return open(ruleId, "Same tool arguments repeated beyond threshold.", evidence);
|
|
103
|
+
}
|
|
104
|
+
function evaluateMaxLoopIterations(events, maxIterations) {
|
|
105
|
+
const ruleId = "circuit.max-loop-iterations";
|
|
106
|
+
const iterationEvents = events.filter((event) => {
|
|
107
|
+
const attrs = event.attributes ?? {};
|
|
108
|
+
return typeof attrs.iteration === "number" || event.name.toLowerCase().includes("loop");
|
|
109
|
+
});
|
|
110
|
+
const maxSeen = iterationEvents.reduce((max, event) => {
|
|
111
|
+
const attrs = event.attributes ?? {};
|
|
112
|
+
const iteration = typeof attrs.iteration === "number" ? attrs.iteration : max;
|
|
113
|
+
return Math.max(max, iteration);
|
|
114
|
+
}, iterationEvents.length);
|
|
115
|
+
if (maxSeen <= maxIterations) {
|
|
116
|
+
return closed(ruleId, "Loop iterations within threshold.");
|
|
117
|
+
}
|
|
118
|
+
return open(ruleId, "Loop iterations exceeded threshold.", [
|
|
119
|
+
{ ruleId, count: maxSeen, threshold: maxIterations }
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
function evaluateMaxRetries(events, maxRetries) {
|
|
123
|
+
const ruleId = "circuit.max-retries";
|
|
124
|
+
const attempts = events.map(attemptNumber).filter((value) => value !== void 0);
|
|
125
|
+
const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;
|
|
126
|
+
if (maxAttempt <= maxRetries) {
|
|
127
|
+
return closed(ruleId, "Retry count within threshold.");
|
|
128
|
+
}
|
|
129
|
+
return open(ruleId, "Retry count exceeded threshold.", [
|
|
130
|
+
{ ruleId, count: maxAttempt, threshold: maxRetries }
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
function evaluateToolTimeout(events, maxDurationMs) {
|
|
134
|
+
const ruleId = "circuit.tool-timeout";
|
|
135
|
+
const evidence = [];
|
|
136
|
+
for (const event of events.filter(isToolEvent)) {
|
|
137
|
+
const duration = durationMs(event);
|
|
138
|
+
if (duration !== void 0 && duration > maxDurationMs) {
|
|
139
|
+
evidence.push({
|
|
140
|
+
ruleId,
|
|
141
|
+
toolName: toolLabel(event),
|
|
142
|
+
count: duration,
|
|
143
|
+
threshold: maxDurationMs,
|
|
144
|
+
eventId: event.eventId
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (evidence.length === 0) {
|
|
149
|
+
return closed(ruleId, "Tool durations within timeout.");
|
|
150
|
+
}
|
|
151
|
+
return open(ruleId, "Tool call exceeded configured timeout.", evidence, "warning");
|
|
152
|
+
}
|
|
153
|
+
function evaluateRunawayLlmLoop(events, maxLlmCalls) {
|
|
154
|
+
const ruleId = "circuit.runaway-llm-loop";
|
|
155
|
+
const llmCount = events.filter(isLlmEvent).length;
|
|
156
|
+
const hasTerminal = events.some((event) => {
|
|
157
|
+
const status = (event.status ?? event.attributes?.status ?? "").toString().toLowerCase();
|
|
158
|
+
return status === "ok" || status === "success" || status === "completed";
|
|
159
|
+
});
|
|
160
|
+
if (llmCount <= maxLlmCalls || hasTerminal) {
|
|
161
|
+
return closed(ruleId, "LLM call count within threshold or run completed.");
|
|
162
|
+
}
|
|
163
|
+
return open(ruleId, "Runaway LLM loop detected.", [
|
|
164
|
+
{ ruleId, count: llmCount, threshold: maxLlmCalls }
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
function evaluateExcessiveBranchWidth(events, maxWidth) {
|
|
168
|
+
const ruleId = "circuit.excessive-branch-width";
|
|
169
|
+
const children = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const event of events) {
|
|
171
|
+
const parentId = event.parentId;
|
|
172
|
+
if (!parentId) continue;
|
|
173
|
+
children.set(parentId, (children.get(parentId) ?? 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
const evidence = [];
|
|
176
|
+
for (const [parentId, count] of children) {
|
|
177
|
+
if (count > maxWidth) {
|
|
178
|
+
evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (evidence.length === 0) {
|
|
182
|
+
return closed(ruleId, "Branch width within threshold.");
|
|
183
|
+
}
|
|
184
|
+
return open(ruleId, "Excessive parallel branch width detected.", evidence, "warning");
|
|
185
|
+
}
|
|
186
|
+
function runRule(ruleId, events, options) {
|
|
187
|
+
switch (ruleId) {
|
|
188
|
+
case "circuit.same-tool-repetition":
|
|
189
|
+
if (options.sameToolRepetition === void 0) return void 0;
|
|
190
|
+
return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);
|
|
191
|
+
case "circuit.same-args-repetition":
|
|
192
|
+
if (options.sameArgsRepetition === void 0) return void 0;
|
|
193
|
+
return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);
|
|
194
|
+
case "circuit.max-loop-iterations":
|
|
195
|
+
if (options.maxLoopIterations === void 0) return void 0;
|
|
196
|
+
return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);
|
|
197
|
+
case "circuit.max-retries":
|
|
198
|
+
if (options.maxRetries === void 0) return void 0;
|
|
199
|
+
return evaluateMaxRetries(events, options.maxRetries.maxRetries);
|
|
200
|
+
case "circuit.tool-timeout":
|
|
201
|
+
if (options.toolTimeout === void 0) return void 0;
|
|
202
|
+
return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);
|
|
203
|
+
case "circuit.runaway-llm-loop":
|
|
204
|
+
if (options.runawayLlmLoop === void 0) return void 0;
|
|
205
|
+
return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);
|
|
206
|
+
case "circuit.excessive-branch-width":
|
|
207
|
+
if (options.excessiveBranchWidth === void 0) return void 0;
|
|
208
|
+
return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);
|
|
209
|
+
default:
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function runCircuits(events, options = {}) {
|
|
214
|
+
const selected = options.rules ?? ALL_RULES;
|
|
215
|
+
const results = [];
|
|
216
|
+
for (const ruleId of selected) {
|
|
217
|
+
const result = runRule(ruleId, events, options);
|
|
218
|
+
if (result) results.push(result);
|
|
219
|
+
}
|
|
220
|
+
const ok = !results.some((result) => result.status === "open" && result.severity === "error");
|
|
221
|
+
return { ok, results };
|
|
222
|
+
}
|
|
223
|
+
var DEFAULT_REDACT_KEYS = [
|
|
224
|
+
"authorization",
|
|
225
|
+
"cookie",
|
|
226
|
+
"token",
|
|
227
|
+
"apiKey",
|
|
228
|
+
"password",
|
|
229
|
+
"secret",
|
|
230
|
+
"email"
|
|
231
|
+
];
|
|
232
|
+
var SHARE_PROFILE_EXTRA_KEYS = [
|
|
233
|
+
"userEmail",
|
|
234
|
+
"customerEmail",
|
|
235
|
+
"phone",
|
|
236
|
+
"phoneNumber",
|
|
237
|
+
"address",
|
|
238
|
+
"ip",
|
|
239
|
+
"ipAddress",
|
|
240
|
+
"sessionId",
|
|
241
|
+
"requestId",
|
|
242
|
+
"correlationId",
|
|
243
|
+
"decisionId",
|
|
244
|
+
"groupId",
|
|
245
|
+
"customerId",
|
|
246
|
+
"userId",
|
|
247
|
+
"accountId",
|
|
248
|
+
"tenantId",
|
|
249
|
+
"orgId",
|
|
250
|
+
"organizationId",
|
|
251
|
+
"traceId",
|
|
252
|
+
"spanId",
|
|
253
|
+
"parentSpanId"
|
|
254
|
+
];
|
|
255
|
+
var STRICT_PROFILE_EXTRA_KEYS = [
|
|
256
|
+
"prompt",
|
|
257
|
+
"completion",
|
|
258
|
+
"input",
|
|
259
|
+
"output",
|
|
260
|
+
"inputPreview",
|
|
261
|
+
"outputPreview",
|
|
262
|
+
"message",
|
|
263
|
+
"messages",
|
|
264
|
+
"transcript",
|
|
265
|
+
"context",
|
|
266
|
+
"document",
|
|
267
|
+
"documents",
|
|
268
|
+
"chunk",
|
|
269
|
+
"chunks",
|
|
270
|
+
"retrieval",
|
|
271
|
+
"query"
|
|
272
|
+
];
|
|
273
|
+
function isRecord(value) {
|
|
274
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
275
|
+
}
|
|
276
|
+
function toKey(key) {
|
|
277
|
+
return key.toLowerCase();
|
|
278
|
+
}
|
|
279
|
+
function stableHash(value) {
|
|
280
|
+
const hash = crypto__default.default.createHash("sha256").update(value, "utf8").digest("hex");
|
|
281
|
+
return hash.slice(0, 8);
|
|
282
|
+
}
|
|
283
|
+
function stringifyScalar(value) {
|
|
284
|
+
if (typeof value === "string") return value;
|
|
285
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
286
|
+
return String(value);
|
|
287
|
+
}
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
function patternDetector(options) {
|
|
291
|
+
return {
|
|
292
|
+
id: options.id,
|
|
293
|
+
severity: options.severity ?? "warning",
|
|
294
|
+
matchKind: "value",
|
|
295
|
+
detect(input) {
|
|
296
|
+
if (typeof input.value !== "string") return [];
|
|
297
|
+
options.pattern.lastIndex = 0;
|
|
298
|
+
return options.pattern.test(input.value) ? [{ action: "replace", severity: options.severity ?? "warning", matchKind: "value" }] : [];
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function digitsOnly(value) {
|
|
303
|
+
return value.replace(/\D/g, "");
|
|
304
|
+
}
|
|
305
|
+
function passesLuhn(value) {
|
|
306
|
+
const digits = digitsOnly(value);
|
|
307
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
308
|
+
let sum = 0;
|
|
309
|
+
let double = false;
|
|
310
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
311
|
+
let digit = Number(digits[i]);
|
|
312
|
+
if (double) {
|
|
313
|
+
digit *= 2;
|
|
314
|
+
if (digit > 9) digit -= 9;
|
|
315
|
+
}
|
|
316
|
+
sum += digit;
|
|
317
|
+
double = !double;
|
|
318
|
+
}
|
|
319
|
+
return sum % 10 === 0;
|
|
320
|
+
}
|
|
321
|
+
var credentialDetectors = [
|
|
322
|
+
patternDetector({
|
|
323
|
+
id: "value.authorizationHeader",
|
|
324
|
+
pattern: /^(?:basic|bearer|digest|apikey)\s+[a-z0-9._~+/=-]+$/i,
|
|
325
|
+
severity: "error"
|
|
326
|
+
}),
|
|
327
|
+
patternDetector({
|
|
328
|
+
id: "value.bearerToken",
|
|
329
|
+
pattern: /\bbearer\s+[a-z0-9._~+/=-]{12,}\b/i,
|
|
330
|
+
severity: "error"
|
|
331
|
+
}),
|
|
332
|
+
patternDetector({
|
|
333
|
+
id: "value.cookie",
|
|
334
|
+
pattern: /\b[a-z0-9_.-]+=[^;\s]+(?:;\s*[a-z0-9_.-]+=[^;\s]+)+/i,
|
|
335
|
+
severity: "error"
|
|
336
|
+
}),
|
|
337
|
+
patternDetector({
|
|
338
|
+
id: "value.jwt",
|
|
339
|
+
pattern: /\beyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\b/,
|
|
340
|
+
severity: "error"
|
|
341
|
+
}),
|
|
342
|
+
patternDetector({
|
|
343
|
+
id: "value.providerApiKey",
|
|
344
|
+
pattern: /\b(?:sk-(?:proj-)?[a-zA-Z0-9_-]{16,}|sk-ant-[a-zA-Z0-9_-]{16,}|AIza[0-9A-Za-z_-]{20,})\b/,
|
|
345
|
+
severity: "error"
|
|
346
|
+
}),
|
|
347
|
+
patternDetector({
|
|
348
|
+
id: "value.githubToken",
|
|
349
|
+
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/,
|
|
350
|
+
severity: "error"
|
|
351
|
+
}),
|
|
352
|
+
patternDetector({
|
|
353
|
+
id: "value.awsAccessKey",
|
|
354
|
+
pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
355
|
+
severity: "error"
|
|
356
|
+
}),
|
|
357
|
+
patternDetector({
|
|
358
|
+
id: "value.privateKey",
|
|
359
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*-----END [A-Z ]*PRIVATE KEY-----/,
|
|
360
|
+
severity: "error"
|
|
361
|
+
}),
|
|
362
|
+
{
|
|
363
|
+
id: "value.creditCard",
|
|
364
|
+
severity: "error",
|
|
365
|
+
matchKind: "value",
|
|
366
|
+
detect(input) {
|
|
367
|
+
if (typeof input.value !== "string") return [];
|
|
368
|
+
const candidatePattern = /(?:\d[ -]?){13,19}/g;
|
|
369
|
+
for (const match of input.value.matchAll(candidatePattern)) {
|
|
370
|
+
const candidate = match[0] ?? "";
|
|
371
|
+
if (passesLuhn(candidate)) {
|
|
372
|
+
return [{ action: "replace", severity: "error", matchKind: "value" }];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
];
|
|
379
|
+
var identifierDetectors = [
|
|
380
|
+
patternDetector({
|
|
381
|
+
id: "value.email",
|
|
382
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
|
|
383
|
+
}),
|
|
384
|
+
patternDetector({
|
|
385
|
+
id: "value.phone",
|
|
386
|
+
pattern: /\b(?:\+\d{1,3}[\s.-]?)?(?:\(?\d{3}\)?[\s.-])\d{3}[\s.-]\d{4}\b/
|
|
387
|
+
}),
|
|
388
|
+
patternDetector({
|
|
389
|
+
id: "value.ipv4",
|
|
390
|
+
pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/
|
|
391
|
+
}),
|
|
392
|
+
patternDetector({
|
|
393
|
+
id: "value.ipv6",
|
|
394
|
+
pattern: /\b(?:[0-9a-f]{1,4}:){2,7}[0-9a-f]{1,4}\b/i
|
|
395
|
+
})
|
|
396
|
+
];
|
|
397
|
+
function builtInDetectorsForProfile(profile) {
|
|
398
|
+
if (profile === "local") return credentialDetectors;
|
|
399
|
+
return [...credentialDetectors, ...identifierDetectors];
|
|
400
|
+
}
|
|
401
|
+
function compileRules(rules, extraKeys) {
|
|
402
|
+
const out = /* @__PURE__ */ new Map();
|
|
403
|
+
const set = (rule) => {
|
|
404
|
+
const key = toKey(rule.key);
|
|
405
|
+
out.set(key, { ...rule, key });
|
|
406
|
+
};
|
|
407
|
+
for (const key of DEFAULT_REDACT_KEYS) {
|
|
408
|
+
set({ key, strategy: "full" });
|
|
409
|
+
}
|
|
410
|
+
for (const key of extraKeys ?? []) {
|
|
411
|
+
if (typeof key === "string" && key.length > 0) {
|
|
412
|
+
set({ key, strategy: "full" });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const rule of rules ?? []) {
|
|
416
|
+
if (typeof rule === "string") {
|
|
417
|
+
set({ key: rule, strategy: "full" });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (rule.strategy === "full") set({ key: rule.key, strategy: "full" });
|
|
421
|
+
if (rule.strategy === "hash") set({ key: rule.key, strategy: "hash" });
|
|
422
|
+
if (rule.strategy === "prefix") {
|
|
423
|
+
set({
|
|
424
|
+
key: rule.key,
|
|
425
|
+
strategy: "prefix",
|
|
426
|
+
keep: typeof rule.keep === "number" ? rule.keep : 8
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return [...out.values()];
|
|
431
|
+
}
|
|
432
|
+
function actionForRule(rule) {
|
|
433
|
+
if (rule.strategy === "full") return "replace";
|
|
434
|
+
return rule.strategy;
|
|
435
|
+
}
|
|
436
|
+
function applyRule(rule, value, replacement) {
|
|
437
|
+
if (rule.strategy === "full") return replacement;
|
|
438
|
+
const asString = stringifyScalar(value);
|
|
439
|
+
if (rule.strategy === "prefix") {
|
|
440
|
+
if (asString === void 0) return replacement;
|
|
441
|
+
const keep = Math.max(0, Math.floor(rule.keep));
|
|
442
|
+
return asString.length <= keep ? `${asString}\u2026` : `${asString.slice(0, keep)}\u2026`;
|
|
443
|
+
}
|
|
444
|
+
if (rule.strategy === "hash") {
|
|
445
|
+
if (asString === void 0) return "[HASH:unknown]";
|
|
446
|
+
return `[HASH:${stableHash(asString)}]`;
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
function childPath(path, key) {
|
|
451
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
|
|
452
|
+
return path ? `${path}.${key}` : key;
|
|
453
|
+
}
|
|
454
|
+
return `${path || "$"}[${JSON.stringify(key)}]`;
|
|
455
|
+
}
|
|
456
|
+
function indexPath(path, index) {
|
|
457
|
+
return `${path || "$"}[${index}]`;
|
|
458
|
+
}
|
|
459
|
+
function makeFinding(path, detector, action, matchKind, severity = "warning", preview) {
|
|
460
|
+
return preview === void 0 ? { path, detector, action, severity, matchKind } : { path, detector, action, severity, matchKind, preview };
|
|
461
|
+
}
|
|
462
|
+
function createRedactionProfile(profile = "local") {
|
|
463
|
+
switch (profile) {
|
|
464
|
+
case "local":
|
|
465
|
+
return { profile: "local", extraKeys: [] };
|
|
466
|
+
case "share":
|
|
467
|
+
return {
|
|
468
|
+
profile: "share",
|
|
469
|
+
extraKeys: SHARE_PROFILE_EXTRA_KEYS,
|
|
470
|
+
maxMetadataValueLengthCap: 500,
|
|
471
|
+
maxPreviewLengthCap: 200
|
|
472
|
+
};
|
|
473
|
+
case "strict":
|
|
474
|
+
return {
|
|
475
|
+
profile: "strict",
|
|
476
|
+
extraKeys: [...SHARE_PROFILE_EXTRA_KEYS, ...STRICT_PROFILE_EXTRA_KEYS],
|
|
477
|
+
maxMetadataValueLengthCap: 200,
|
|
478
|
+
maxPreviewLengthCap: 80
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
var Redactor = class {
|
|
483
|
+
#rules;
|
|
484
|
+
#detectors;
|
|
485
|
+
#profile;
|
|
486
|
+
#replacement;
|
|
487
|
+
#maxDepth;
|
|
488
|
+
#collectFindings;
|
|
489
|
+
constructor(options) {
|
|
490
|
+
const resolved = createRedactionProfile(options?.profile ?? "local");
|
|
491
|
+
this.#profile = resolved.profile;
|
|
492
|
+
this.#rules = compileRules(options?.rules, [
|
|
493
|
+
...resolved.extraKeys,
|
|
494
|
+
...options?.extraKeys ?? []
|
|
495
|
+
]);
|
|
496
|
+
this.#detectors = [
|
|
497
|
+
...builtInDetectorsForProfile(this.#profile),
|
|
498
|
+
...options?.detectors ?? []
|
|
499
|
+
];
|
|
500
|
+
this.#replacement = options?.replacement ?? "[REDACTED]";
|
|
501
|
+
this.#maxDepth = options?.maxDepth ?? 32;
|
|
502
|
+
this.#collectFindings = options?.collectFindings ?? true;
|
|
503
|
+
}
|
|
504
|
+
redactValue(key, value) {
|
|
505
|
+
return this.#redactValue(value, key, key, 0, {
|
|
506
|
+
findings: [],
|
|
507
|
+
seen: /* @__PURE__ */ new WeakMap()
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
redactRecord(record) {
|
|
511
|
+
return this.redact(record).value;
|
|
512
|
+
}
|
|
513
|
+
redact(value) {
|
|
514
|
+
const state = {
|
|
515
|
+
findings: [],
|
|
516
|
+
seen: /* @__PURE__ */ new WeakMap()
|
|
517
|
+
};
|
|
518
|
+
const redacted = this.#redactValue(value, void 0, "$", 0, state);
|
|
519
|
+
return {
|
|
520
|
+
value: redacted,
|
|
521
|
+
findings: state.findings,
|
|
522
|
+
redacted: state.findings.some((finding) => finding.action !== "keep"),
|
|
523
|
+
profile: this.#profile
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
#recordFinding(state, finding) {
|
|
527
|
+
if (this.#collectFindings) state.findings.push(finding);
|
|
528
|
+
}
|
|
529
|
+
#redactValue(value, key, path, depth, state) {
|
|
530
|
+
if (depth > this.#maxDepth) {
|
|
531
|
+
this.#recordFinding(
|
|
532
|
+
state,
|
|
533
|
+
makeFinding(path, "structure.maxDepth", "truncate", "value", "warning")
|
|
534
|
+
);
|
|
535
|
+
return "[Truncated]";
|
|
536
|
+
}
|
|
537
|
+
if (key !== void 0) {
|
|
538
|
+
const rule = this.#rules.find((candidate) => candidate.key === toKey(key));
|
|
539
|
+
if (rule) {
|
|
540
|
+
this.#recordFinding(
|
|
541
|
+
state,
|
|
542
|
+
makeFinding(path, `key.${rule.key}`, actionForRule(rule), "key", "warning")
|
|
543
|
+
);
|
|
544
|
+
return applyRule(rule, value, this.#replacement);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const detector of this.#detectors) {
|
|
548
|
+
const detections = detector.detect({ path, key, value });
|
|
549
|
+
for (const detection of detections) {
|
|
550
|
+
const action = detection.action ?? "replace";
|
|
551
|
+
this.#recordFinding(
|
|
552
|
+
state,
|
|
553
|
+
makeFinding(
|
|
554
|
+
path,
|
|
555
|
+
detector.id,
|
|
556
|
+
action,
|
|
557
|
+
detection.matchKind ?? detector.matchKind ?? "custom",
|
|
558
|
+
detection.severity ?? detector.severity ?? "warning",
|
|
559
|
+
detection.preview
|
|
560
|
+
)
|
|
561
|
+
);
|
|
562
|
+
if (action !== "keep") {
|
|
563
|
+
return detection.replacement ?? this.#replacement;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (Array.isArray(value)) {
|
|
568
|
+
if (state.seen.has(value)) return state.seen.get(value);
|
|
569
|
+
const out = [];
|
|
570
|
+
state.seen.set(value, out);
|
|
571
|
+
value.forEach((item, index) => {
|
|
572
|
+
out[index] = this.#redactValue(item, void 0, indexPath(path, index), depth + 1, state);
|
|
573
|
+
});
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
if (isRecord(value)) {
|
|
577
|
+
if (state.seen.has(value)) return state.seen.get(value);
|
|
578
|
+
const out = {};
|
|
579
|
+
state.seen.set(value, out);
|
|
580
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
581
|
+
out[entryKey] = this.#redactValue(
|
|
582
|
+
entryValue,
|
|
583
|
+
entryKey,
|
|
584
|
+
childPath(path === "$" ? "" : path, entryKey),
|
|
585
|
+
depth + 1,
|
|
586
|
+
state
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
function createRedactor(options) {
|
|
595
|
+
return new Redactor(options);
|
|
596
|
+
}
|
|
597
|
+
function redact(value, options) {
|
|
598
|
+
return createRedactor(options).redact(value);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// packages/guardrails/src/rules.ts
|
|
602
|
+
var DEFAULT_INJECTION_PATTERNS = [
|
|
603
|
+
"ignore previous instructions",
|
|
604
|
+
"ignore all prior",
|
|
605
|
+
"disregard your instructions",
|
|
606
|
+
"system prompt",
|
|
607
|
+
"you are now",
|
|
608
|
+
"jailbreak"
|
|
609
|
+
];
|
|
610
|
+
function pass(ruleId, message) {
|
|
611
|
+
return { ruleId, status: "pass", severity: "info", message, evidence: [] };
|
|
612
|
+
}
|
|
613
|
+
function fail(ruleId, message, evidence, severity = "error") {
|
|
614
|
+
return { ruleId, status: severity === "warning" ? "warn" : "fail", severity, message, evidence };
|
|
615
|
+
}
|
|
616
|
+
function boundedPreview(value, max = 80) {
|
|
617
|
+
if (value.length <= max) return value;
|
|
618
|
+
return `${value.slice(0, max - 3)}...`;
|
|
619
|
+
}
|
|
620
|
+
function evaluateBannedPhrase(text, options) {
|
|
621
|
+
const ruleId = "guardrail.banned-phrase";
|
|
622
|
+
const haystack = options.caseInsensitive !== false ? text.toLowerCase() : text;
|
|
623
|
+
const evidence = [];
|
|
624
|
+
for (const phrase of options.phrases) {
|
|
625
|
+
const needle = options.caseInsensitive !== false ? phrase.toLowerCase() : phrase;
|
|
626
|
+
if (needle.length > 0 && haystack.includes(needle)) {
|
|
627
|
+
evidence.push({ ruleId, match: phrase, preview: boundedPreview(text) });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (evidence.length === 0) {
|
|
631
|
+
return pass(ruleId, "No banned phrases matched.");
|
|
632
|
+
}
|
|
633
|
+
return fail(ruleId, `Matched ${evidence.length} banned phrase(s).`, evidence);
|
|
634
|
+
}
|
|
635
|
+
var SEVERITY_RANK = { info: 0, warning: 1, error: 2 };
|
|
636
|
+
function evaluatePiiLeak(value, options = {}) {
|
|
637
|
+
const ruleId = "guardrail.pii-leak";
|
|
638
|
+
const minSeverity = options.minSeverity ?? "warning";
|
|
639
|
+
const result = redact(value, { profile: options.profile ?? "share", collectFindings: true });
|
|
640
|
+
const findings = result.findings.filter(
|
|
641
|
+
(finding) => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[minSeverity]
|
|
642
|
+
);
|
|
643
|
+
if (findings.length === 0) {
|
|
644
|
+
return pass(ruleId, "No PII-style redaction findings.");
|
|
645
|
+
}
|
|
646
|
+
const evidence = findings.map((finding) => ({
|
|
647
|
+
ruleId,
|
|
648
|
+
path: finding.path,
|
|
649
|
+
detector: finding.detector,
|
|
650
|
+
preview: finding.preview
|
|
651
|
+
}));
|
|
652
|
+
return fail(ruleId, `Detected ${findings.length} PII-style finding(s).`, evidence);
|
|
653
|
+
}
|
|
654
|
+
function measureDepth(value) {
|
|
655
|
+
if (value === null || typeof value !== "object") return 0;
|
|
656
|
+
if (Array.isArray(value)) {
|
|
657
|
+
return 1 + Math.max(0, ...value.map((item) => measureDepth(item)));
|
|
658
|
+
}
|
|
659
|
+
const depths = Object.values(value).map((item) => measureDepth(item));
|
|
660
|
+
return 1 + (depths.length === 0 ? 0 : Math.max(...depths));
|
|
661
|
+
}
|
|
662
|
+
function maxStringLength(value) {
|
|
663
|
+
if (typeof value === "string") return value.length;
|
|
664
|
+
if (value === null || typeof value !== "object") return 0;
|
|
665
|
+
if (Array.isArray(value)) {
|
|
666
|
+
return Math.max(0, ...value.map((item) => maxStringLength(item)));
|
|
667
|
+
}
|
|
668
|
+
return Math.max(0, ...Object.values(value).map((item) => maxStringLength(item)));
|
|
669
|
+
}
|
|
670
|
+
function evaluateUnsafeToolArgs(toolName, toolArgs2, options = {}) {
|
|
671
|
+
const ruleId = "guardrail.unsafe-tool-args";
|
|
672
|
+
const blocked = new Set((options.blockedTools ?? []).map((name) => name.toLowerCase()));
|
|
673
|
+
const evidence = [];
|
|
674
|
+
if (blocked.has(toolName.toLowerCase())) {
|
|
675
|
+
evidence.push({ ruleId, preview: toolName, match: toolName });
|
|
676
|
+
}
|
|
677
|
+
const maxDepth = options.maxDepth ?? 12;
|
|
678
|
+
const depth = measureDepth(toolArgs2);
|
|
679
|
+
if (depth > maxDepth) {
|
|
680
|
+
evidence.push({ ruleId, path: "args", preview: `depth=${depth}` });
|
|
681
|
+
}
|
|
682
|
+
const maxLen = options.maxStringLength ?? 16384;
|
|
683
|
+
const longest = maxStringLength(toolArgs2);
|
|
684
|
+
if (longest > maxLen) {
|
|
685
|
+
evidence.push({ ruleId, path: "args", preview: `maxStringLength=${longest}` });
|
|
686
|
+
}
|
|
687
|
+
if (evidence.length === 0) {
|
|
688
|
+
return pass(ruleId, "Tool arguments within configured bounds.");
|
|
689
|
+
}
|
|
690
|
+
return fail(ruleId, "Unsafe or oversized tool arguments detected.", evidence);
|
|
691
|
+
}
|
|
692
|
+
function evaluatePromptInjection(text, options = {}) {
|
|
693
|
+
const ruleId = "guardrail.prompt-injection";
|
|
694
|
+
const patterns = options.patterns ?? DEFAULT_INJECTION_PATTERNS;
|
|
695
|
+
const haystack = text.toLowerCase();
|
|
696
|
+
const evidence = [];
|
|
697
|
+
for (const pattern of patterns) {
|
|
698
|
+
const needle = pattern.toLowerCase();
|
|
699
|
+
if (needle.length > 0 && haystack.includes(needle)) {
|
|
700
|
+
evidence.push({ ruleId, match: pattern, preview: boundedPreview(text) });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (evidence.length === 0) {
|
|
704
|
+
return pass(ruleId, "No prompt-injection patterns matched.");
|
|
705
|
+
}
|
|
706
|
+
return fail(ruleId, `Matched ${evidence.length} injection pattern(s).`, evidence, "warning");
|
|
707
|
+
}
|
|
708
|
+
function validateSchemaField(value, field, path, evidence) {
|
|
709
|
+
const ruleId = "guardrail.structured-output";
|
|
710
|
+
if (field.type) {
|
|
711
|
+
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
712
|
+
if (actual !== field.type) {
|
|
713
|
+
evidence.push({ ruleId, path, preview: `expected ${field.type}, got ${actual}` });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (field.enum && !field.enum.some((item) => Object.is(item, value))) {
|
|
718
|
+
evidence.push({ ruleId, path, preview: "value not in enum" });
|
|
719
|
+
}
|
|
720
|
+
if (field.type === "object" && field.required && value && typeof value === "object" && !Array.isArray(value)) {
|
|
721
|
+
const record = value;
|
|
722
|
+
for (const key of field.required) {
|
|
723
|
+
if (!(key in record)) {
|
|
724
|
+
evidence.push({ ruleId, path: `${path}.${key}`, preview: "missing required key" });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function evaluateStructuredOutput(value, options) {
|
|
730
|
+
const ruleId = "guardrail.structured-output";
|
|
731
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
732
|
+
return fail(ruleId, "Structured output must be an object.", [
|
|
733
|
+
{ ruleId, preview: typeof value }
|
|
734
|
+
]);
|
|
735
|
+
}
|
|
736
|
+
const record = value;
|
|
737
|
+
const evidence = [];
|
|
738
|
+
for (const [key, field] of Object.entries(options.schema)) {
|
|
739
|
+
validateSchemaField(record[key], field, key, evidence);
|
|
740
|
+
}
|
|
741
|
+
if (evidence.length === 0) {
|
|
742
|
+
return pass(ruleId, "Structured output matches schema subset.");
|
|
743
|
+
}
|
|
744
|
+
return fail(ruleId, "Structured output schema violation.", evidence);
|
|
745
|
+
}
|
|
746
|
+
function evaluateOversizeOutput(value, options = {}) {
|
|
747
|
+
const ruleId = "guardrail.oversize-output";
|
|
748
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
749
|
+
const maxLength = options.maxLength ?? options.maxSerializedLength ?? 32768;
|
|
750
|
+
if (text.length <= maxLength) {
|
|
751
|
+
return pass(ruleId, "Output within size limits.");
|
|
752
|
+
}
|
|
753
|
+
return fail(ruleId, `Output exceeds max length (${text.length} > ${maxLength}).`, [
|
|
754
|
+
{ ruleId, preview: `length=${text.length}` }
|
|
755
|
+
]);
|
|
756
|
+
}
|
|
757
|
+
function evaluateRequiredJsonShape(value, options) {
|
|
758
|
+
const ruleId = "guardrail.required-json-shape";
|
|
759
|
+
let parsed = value;
|
|
760
|
+
if (typeof value === "string") {
|
|
761
|
+
try {
|
|
762
|
+
parsed = JSON.parse(value);
|
|
763
|
+
} catch {
|
|
764
|
+
return fail(ruleId, "Value is not valid JSON.", [{ ruleId, preview: boundedPreview(value) }]);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
768
|
+
return fail(ruleId, "JSON value must be an object.", [{ ruleId, preview: typeof parsed }]);
|
|
769
|
+
}
|
|
770
|
+
const record = parsed;
|
|
771
|
+
const evidence = [];
|
|
772
|
+
for (const key of options.requiredKeys) {
|
|
773
|
+
if (!(key in record)) {
|
|
774
|
+
evidence.push({ ruleId, path: key, preview: "missing required key" });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (evidence.length === 0) {
|
|
778
|
+
return pass(ruleId, "Required JSON keys present.");
|
|
779
|
+
}
|
|
780
|
+
return fail(ruleId, "Missing required JSON keys.", evidence);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// packages/guardrails/src/run.ts
|
|
784
|
+
var ALL_RULES2 = [
|
|
785
|
+
"guardrail.banned-phrase",
|
|
786
|
+
"guardrail.pii-leak",
|
|
787
|
+
"guardrail.unsafe-tool-args",
|
|
788
|
+
"guardrail.prompt-injection",
|
|
789
|
+
"guardrail.structured-output",
|
|
790
|
+
"guardrail.oversize-output",
|
|
791
|
+
"guardrail.required-json-shape"
|
|
792
|
+
];
|
|
793
|
+
function isErrorFailure(result) {
|
|
794
|
+
return result.status === "fail" && result.severity === "error";
|
|
795
|
+
}
|
|
796
|
+
function runRule2(ruleId, input, options) {
|
|
797
|
+
switch (ruleId) {
|
|
798
|
+
case "guardrail.banned-phrase": {
|
|
799
|
+
if (!options.bannedPhrase || input.text === void 0) return void 0;
|
|
800
|
+
return evaluateBannedPhrase(input.text, options.bannedPhrase);
|
|
801
|
+
}
|
|
802
|
+
case "guardrail.pii-leak": {
|
|
803
|
+
if (input.value === void 0 && input.text === void 0) return void 0;
|
|
804
|
+
return evaluatePiiLeak(input.value ?? input.text, options.piiLeak);
|
|
805
|
+
}
|
|
806
|
+
case "guardrail.unsafe-tool-args": {
|
|
807
|
+
if (!input.toolName) return void 0;
|
|
808
|
+
return evaluateUnsafeToolArgs(input.toolName, input.toolArgs ?? {}, options.unsafeToolArgs);
|
|
809
|
+
}
|
|
810
|
+
case "guardrail.prompt-injection": {
|
|
811
|
+
if (input.text === void 0) return void 0;
|
|
812
|
+
return evaluatePromptInjection(input.text, options.promptInjection);
|
|
813
|
+
}
|
|
814
|
+
case "guardrail.structured-output": {
|
|
815
|
+
if (!options.structuredOutput || input.value === void 0) return void 0;
|
|
816
|
+
return evaluateStructuredOutput(input.value, options.structuredOutput);
|
|
817
|
+
}
|
|
818
|
+
case "guardrail.oversize-output": {
|
|
819
|
+
if (input.value === void 0 && input.text === void 0) return void 0;
|
|
820
|
+
return evaluateOversizeOutput(input.value ?? input.text, options.oversizeOutput);
|
|
821
|
+
}
|
|
822
|
+
case "guardrail.required-json-shape": {
|
|
823
|
+
if (!options.requiredJsonShape || input.value === void 0 && input.text === void 0) return void 0;
|
|
824
|
+
return evaluateRequiredJsonShape(input.value ?? input.text, options.requiredJsonShape);
|
|
825
|
+
}
|
|
826
|
+
default:
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function runGuardrails(input, options = {}) {
|
|
831
|
+
const selected = options.rules ?? ALL_RULES2;
|
|
832
|
+
const results = [];
|
|
833
|
+
for (const ruleId of selected) {
|
|
834
|
+
const result = runRule2(ruleId, input, options);
|
|
835
|
+
if (result) results.push(result);
|
|
836
|
+
}
|
|
837
|
+
const ok = !results.some(isErrorFailure);
|
|
838
|
+
return { ok, results };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// packages/eval/src/safety-rules.ts
|
|
842
|
+
function pass2(ruleId, message) {
|
|
843
|
+
return { ruleId, status: "pass", severity: "info", message, evidence: [] };
|
|
844
|
+
}
|
|
845
|
+
function failFinding(ruleId, message, expected, actual) {
|
|
846
|
+
return {
|
|
847
|
+
ruleId,
|
|
848
|
+
status: "fail",
|
|
849
|
+
severity: "error",
|
|
850
|
+
message,
|
|
851
|
+
expected,
|
|
852
|
+
actual,
|
|
853
|
+
evidence: []
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function warnFinding(ruleId, message, actual) {
|
|
857
|
+
return {
|
|
858
|
+
ruleId,
|
|
859
|
+
status: "warning",
|
|
860
|
+
severity: "warning",
|
|
861
|
+
message,
|
|
862
|
+
actual,
|
|
863
|
+
evidence: []
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function collectAnswerText(context) {
|
|
867
|
+
const parts = [];
|
|
868
|
+
for (const node of context.nodes) {
|
|
869
|
+
const attrs = node.event.attributes ?? {};
|
|
870
|
+
for (const key of ["answer", "output", "text", "content"]) {
|
|
871
|
+
const value = attrs[key];
|
|
872
|
+
if (typeof value === "string") parts.push(value);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return parts.join("\n");
|
|
876
|
+
}
|
|
877
|
+
function createEvalGuardrailRule(options) {
|
|
878
|
+
return {
|
|
879
|
+
id: "eval.guardrails",
|
|
880
|
+
category: "safety",
|
|
881
|
+
evaluate(context) {
|
|
882
|
+
const text = collectAnswerText(context);
|
|
883
|
+
const run = runGuardrails({ text, value: context.events }, options);
|
|
884
|
+
return run.results.flatMap((result) => {
|
|
885
|
+
if (result.status === "pass") return [pass2(result.ruleId, result.message)];
|
|
886
|
+
if (result.status === "warn") return [warnFinding(result.ruleId, result.message)];
|
|
887
|
+
return [failFinding(result.ruleId, result.message)];
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function createEvalCircuitRule(options) {
|
|
893
|
+
return {
|
|
894
|
+
id: "eval.circuits",
|
|
895
|
+
category: "structure",
|
|
896
|
+
evaluate(context) {
|
|
897
|
+
const events = context.events.map((event) => ({
|
|
898
|
+
eventId: event.eventId,
|
|
899
|
+
runId: event.runId,
|
|
900
|
+
name: event.name,
|
|
901
|
+
kind: event.kind,
|
|
902
|
+
parentId: event.parentId,
|
|
903
|
+
startedAt: event.startedAt,
|
|
904
|
+
endedAt: event.endedAt,
|
|
905
|
+
durationMs: event.durationMs,
|
|
906
|
+
attributes: event.attributes,
|
|
907
|
+
status: event.status
|
|
908
|
+
}));
|
|
909
|
+
const run = runCircuits(events, options);
|
|
910
|
+
return run.results.flatMap((result) => {
|
|
911
|
+
if (result.status === "closed") return [pass2(result.ruleId, result.message)];
|
|
912
|
+
if (result.status === "warn") return [warnFinding(result.ruleId, result.message, result.evidence[0]?.count)];
|
|
913
|
+
return [failFinding(result.ruleId, result.message, result.evidence[0]?.threshold, result.evidence[0]?.count)];
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
4
918
|
|
|
5
919
|
// packages/eval/src/index.ts
|
|
6
920
|
function isTraceReadResult(value) {
|
|
@@ -185,7 +1099,7 @@ function evidenceForEvent(event, path) {
|
|
|
185
1099
|
}
|
|
186
1100
|
];
|
|
187
1101
|
}
|
|
188
|
-
function
|
|
1102
|
+
function fail2(ruleId, message, evidence, expected, actual) {
|
|
189
1103
|
return {
|
|
190
1104
|
ruleId,
|
|
191
1105
|
status: "fail",
|
|
@@ -381,7 +1295,7 @@ var checks = {
|
|
|
381
1295
|
"eval.requireSuccess",
|
|
382
1296
|
"run",
|
|
383
1297
|
(context) => context.run.status === "ok" ? [] : [
|
|
384
|
-
|
|
1298
|
+
fail2(
|
|
385
1299
|
"eval.requireSuccess",
|
|
386
1300
|
"Run did not complete successfully.",
|
|
387
1301
|
evidenceForRun(context.run, "status"),
|
|
@@ -396,7 +1310,7 @@ var checks = {
|
|
|
396
1310
|
return createRule("eval.requiredTools", "tool", (context) => {
|
|
397
1311
|
const tools = new Set(nodeNames(context.nodes, "TOOL"));
|
|
398
1312
|
return expected.filter((name) => !tools.has(name)).map(
|
|
399
|
-
(name) =>
|
|
1313
|
+
(name) => fail2(
|
|
400
1314
|
"eval.requiredTools",
|
|
401
1315
|
`Required tool ${name} did not appear.`,
|
|
402
1316
|
evidenceForRun(context.run, "children"),
|
|
@@ -412,7 +1326,7 @@ var checks = {
|
|
|
412
1326
|
"eval.forbiddenTools",
|
|
413
1327
|
"tool",
|
|
414
1328
|
(context) => context.nodes.filter((node) => node.event.kind === "TOOL" && blocked.includes(node.event.name)).map(
|
|
415
|
-
(node) =>
|
|
1329
|
+
(node) => fail2(
|
|
416
1330
|
"eval.forbiddenTools",
|
|
417
1331
|
`Forbidden tool ${node.event.name} appeared.`,
|
|
418
1332
|
evidenceForEvent(node.event, "name"),
|
|
@@ -427,7 +1341,7 @@ var checks = {
|
|
|
427
1341
|
"eval.maxDurationMs",
|
|
428
1342
|
"run",
|
|
429
1343
|
(context) => context.run.durationMs !== void 0 && context.run.durationMs > maxDurationMs ? [
|
|
430
|
-
|
|
1344
|
+
fail2(
|
|
431
1345
|
"eval.maxDurationMs",
|
|
432
1346
|
`Run duration exceeded ${maxDurationMs}ms.`,
|
|
433
1347
|
evidenceForRun(context.run, "durationMs"),
|
|
@@ -444,7 +1358,7 @@ var checks = {
|
|
|
444
1358
|
void 0
|
|
445
1359
|
);
|
|
446
1360
|
return deepest !== void 0 && deepest.depth > maxDepth ? [
|
|
447
|
-
|
|
1361
|
+
fail2(
|
|
448
1362
|
"eval.maxDepth",
|
|
449
1363
|
`Run tree depth exceeded ${maxDepth}.`,
|
|
450
1364
|
evidenceForEvent(deepest.event, "depth"),
|
|
@@ -461,7 +1375,7 @@ var checks = {
|
|
|
461
1375
|
(context) => context.nodes.flatMap((node) => {
|
|
462
1376
|
const retries = numericAttribute(node, ["retryCount", "retries", "attempt"]);
|
|
463
1377
|
return retries !== void 0 && retries > maxRetries ? [
|
|
464
|
-
|
|
1378
|
+
fail2(
|
|
465
1379
|
"eval.maxRetries",
|
|
466
1380
|
`Retry count exceeded ${maxRetries}.`,
|
|
467
1381
|
evidenceForEvent(node.event, "attributes.retryCount"),
|
|
@@ -476,7 +1390,7 @@ var checks = {
|
|
|
476
1390
|
return createRule("eval.maxTotalTokens", "llm", (context) => {
|
|
477
1391
|
const total = totalTokenCount(context.events);
|
|
478
1392
|
return total > maxTotalTokens ? [
|
|
479
|
-
|
|
1393
|
+
fail2(
|
|
480
1394
|
"eval.maxTotalTokens",
|
|
481
1395
|
`Total token usage exceeded ${maxTotalTokens}.`,
|
|
482
1396
|
evidenceForRun(context.run, "tokenUsage.total"),
|
|
@@ -491,7 +1405,7 @@ var checks = {
|
|
|
491
1405
|
"eval.noFailedSteps",
|
|
492
1406
|
"run",
|
|
493
1407
|
(context) => context.nodes.filter((node) => node.event.status === "error" || node.event.kind === "ERROR").map(
|
|
494
|
-
(node) =>
|
|
1408
|
+
(node) => fail2(
|
|
495
1409
|
"eval.noFailedSteps",
|
|
496
1410
|
"Run contains a failed step or error node.",
|
|
497
1411
|
evidenceForEvent(node.event, "status"),
|
|
@@ -509,7 +1423,7 @@ var checks = {
|
|
|
509
1423
|
(node, index) => index < firstLlmIndex && node.event.kind === "RETRIEVER"
|
|
510
1424
|
);
|
|
511
1425
|
return retrievalIndex === -1 ? [
|
|
512
|
-
|
|
1426
|
+
fail2(
|
|
513
1427
|
"eval.requiredRetrievalBeforeGeneration",
|
|
514
1428
|
"No retrieval step appeared before the first LLM generation.",
|
|
515
1429
|
evidenceForEvent(context.nodes[firstLlmIndex].event, "kind"),
|
|
@@ -525,7 +1439,7 @@ var checks = {
|
|
|
525
1439
|
const decisions = context.nodes.filter((node) => node.event.kind === "DECISION");
|
|
526
1440
|
if (decisions.length === 0) {
|
|
527
1441
|
return [
|
|
528
|
-
|
|
1442
|
+
fail2(
|
|
529
1443
|
"eval.requiredDecisionMetadata",
|
|
530
1444
|
"No decision node is available for required metadata.",
|
|
531
1445
|
evidenceForRun(context.run, "children"),
|
|
@@ -536,7 +1450,7 @@ var checks = {
|
|
|
536
1450
|
}
|
|
537
1451
|
return decisions.flatMap(
|
|
538
1452
|
(node) => required.filter((key) => !hasAttribute(node, key)).map(
|
|
539
|
-
(key) =>
|
|
1453
|
+
(key) => fail2(
|
|
540
1454
|
"eval.requiredDecisionMetadata",
|
|
541
1455
|
`Decision metadata ${key} is missing.`,
|
|
542
1456
|
evidenceForEvent(node.event, `attributes.${key}`),
|
|
@@ -557,7 +1471,7 @@ var checks = {
|
|
|
557
1471
|
const contexts = collectTextFields(context.nodes, contextKeys, ["RETRIEVER", "TOOL"]);
|
|
558
1472
|
if (answers.length === 0 || contexts.length === 0) {
|
|
559
1473
|
return [
|
|
560
|
-
|
|
1474
|
+
fail2(
|
|
561
1475
|
"eval.contextOverlap",
|
|
562
1476
|
"Answer and context text are required for overlap evaluation.",
|
|
563
1477
|
firstEvidence(answers.length > 0 ? answers : contexts, context.run, "children"),
|
|
@@ -571,7 +1485,7 @@ var checks = {
|
|
|
571
1485
|
const sharedTerms = [...answerTerms].filter((term) => contextTerms.has(term)).length;
|
|
572
1486
|
const overlap = answerTerms.size === 0 ? 0 : sharedTerms / answerTerms.size;
|
|
573
1487
|
return sharedTerms < minSharedTerms || overlap < minOverlap ? [
|
|
574
|
-
|
|
1488
|
+
fail2(
|
|
575
1489
|
"eval.contextOverlap",
|
|
576
1490
|
"Answer text did not sufficiently overlap retrieved context.",
|
|
577
1491
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -599,7 +1513,7 @@ var checks = {
|
|
|
599
1513
|
const quotes = quotedSnippets(answerText, minQuoteLength);
|
|
600
1514
|
if (quotes.length === 0) {
|
|
601
1515
|
return requireQuote ? [
|
|
602
|
-
|
|
1516
|
+
fail2(
|
|
603
1517
|
"eval.quoteOverlap",
|
|
604
1518
|
"Answer did not contain a quote for overlap evaluation.",
|
|
605
1519
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -610,7 +1524,7 @@ var checks = {
|
|
|
610
1524
|
}
|
|
611
1525
|
const missing = quotes.filter((quote) => !contextText.includes(quote.toLowerCase()));
|
|
612
1526
|
return missing.length > 0 ? [
|
|
613
|
-
|
|
1527
|
+
fail2(
|
|
614
1528
|
"eval.quoteOverlap",
|
|
615
1529
|
"Quoted answer text did not appear in retrieved context.",
|
|
616
1530
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -628,7 +1542,7 @@ var checks = {
|
|
|
628
1542
|
const citations = collectTextFields(context.nodes, citationKeys);
|
|
629
1543
|
const count = citationCount(answers.map((field) => field.text).join(" "), citations);
|
|
630
1544
|
return count === 0 ? [
|
|
631
|
-
|
|
1545
|
+
fail2(
|
|
632
1546
|
"eval.citationPresence",
|
|
633
1547
|
"Answer did not include citations or source references.",
|
|
634
1548
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -646,7 +1560,7 @@ var checks = {
|
|
|
646
1560
|
const availableSet = new Set(available);
|
|
647
1561
|
const missing = expected.filter((id) => !availableSet.has(id));
|
|
648
1562
|
return missing.length > 0 ? [
|
|
649
|
-
|
|
1563
|
+
fail2(
|
|
650
1564
|
"eval.requiredSourceIds",
|
|
651
1565
|
"Required source IDs were not present in trace context or citations.",
|
|
652
1566
|
evidenceForRun(context.run, "children"),
|
|
@@ -666,7 +1580,7 @@ var checks = {
|
|
|
666
1580
|
const tooShort = options.minCharacters !== void 0 && characters < options.minCharacters || options.minWords !== void 0 && words < options.minWords;
|
|
667
1581
|
const tooLong = options.maxCharacters !== void 0 && characters > options.maxCharacters || options.maxWords !== void 0 && words > options.maxWords;
|
|
668
1582
|
return answer.length === 0 || tooShort || tooLong ? [
|
|
669
|
-
|
|
1583
|
+
fail2(
|
|
670
1584
|
"eval.answerLengthBounds",
|
|
671
1585
|
"Answer length fell outside required bounds.",
|
|
672
1586
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -689,7 +1603,7 @@ var checks = {
|
|
|
689
1603
|
const answer = answers.map((field) => field.text).join(" ").toLowerCase();
|
|
690
1604
|
const matches = banned.filter((phrase) => answer.includes(phrase));
|
|
691
1605
|
return matches.length > 0 ? [
|
|
692
|
-
|
|
1606
|
+
fail2(
|
|
693
1607
|
"eval.bannedUnsupportedPhrases",
|
|
694
1608
|
"Answer contained banned unsupported-answer phrasing.",
|
|
695
1609
|
firstEvidence(answers, context.run, "attributes.answer"),
|
|
@@ -727,6 +1641,8 @@ function renderEvalMarkdown(result) {
|
|
|
727
1641
|
}
|
|
728
1642
|
|
|
729
1643
|
exports.checks = checks;
|
|
1644
|
+
exports.createEvalCircuitRule = createEvalCircuitRule;
|
|
1645
|
+
exports.createEvalGuardrailRule = createEvalGuardrailRule;
|
|
730
1646
|
exports.evalRun = evalRun;
|
|
731
1647
|
exports.renderEvalMarkdown = renderEvalMarkdown;
|
|
732
1648
|
//# sourceMappingURL=index.cjs.map
|