@iris-eval/mcp-server 0.4.6 → 0.5.1
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 +98 -47
- package/dist/audit-log-reader.d.ts +0 -2
- package/dist/audit-log-reader.js +3 -3
- package/dist/config/index.js +18 -1
- package/dist/custom-rule-store.js +22 -8
- package/dist/dashboard/assets/index-UffZ-aEJ.css +1 -0
- package/dist/dashboard/assets/index-VI_nbMfN.js +10 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-greek.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-greek.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-vietnamese.woff2 +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/failures.d.ts +3 -0
- package/dist/dashboard/routes/failures.js +76 -0
- package/dist/dashboard/routes/index.d.ts +1 -0
- package/dist/dashboard/routes/index.js +1 -0
- package/dist/dashboard/routes/preferences.js +7 -2
- package/dist/dashboard/routes/rules.js +32 -14
- package/dist/dashboard/routes/traces.d.ts +12 -1
- package/dist/dashboard/routes/traces.js +99 -2
- package/dist/dashboard/seed-demo-data.d.ts +49 -0
- package/dist/dashboard/seed-demo-data.js +1080 -0
- package/dist/dashboard/server.js +81 -15
- package/dist/dashboard/validation.d.ts +74 -0
- package/dist/dashboard/validation.js +31 -2
- package/dist/eval/citation-verify/resolve.js +29 -0
- package/dist/eval/citation-verify/verifier.d.ts +18 -0
- package/dist/eval/citation-verify/verifier.js +80 -19
- package/dist/eval/decision-moment.js +17 -9
- package/dist/eval/engine.d.ts +15 -1
- package/dist/eval/engine.js +106 -5
- package/dist/eval/failure-rank.d.ts +14 -0
- package/dist/eval/failure-rank.js +44 -0
- package/dist/eval/llm-judge/evaluator.js +50 -33
- package/dist/eval/llm-judge/templates/index.d.ts +4 -0
- package/dist/eval/llm-judge/templates/index.js +10 -4
- package/dist/eval/rules/custom.d.ts +29 -1
- package/dist/eval/rules/custom.js +213 -24
- package/dist/eval/rules/regex-budget.js +0 -0
- package/dist/eval/rules/regex-sandbox.d.ts +26 -0
- package/dist/eval/rules/regex-sandbox.js +131 -0
- package/dist/eval/rules/relevance.d.ts +0 -2
- package/dist/eval/rules/relevance.js +6 -68
- package/dist/eval/rules/safety.d.ts +10 -0
- package/dist/eval/rules/safety.js +1352 -28
- package/dist/index.js +196 -18
- package/dist/self-test.d.ts +18 -0
- package/dist/self-test.js +329 -0
- package/dist/storage/migrations/006-eval-critical-failures.d.ts +3 -0
- package/dist/storage/migrations/006-eval-critical-failures.js +23 -0
- package/dist/storage/migrations/index.js +2 -0
- package/dist/storage/sqlite-adapter.d.ts +2 -0
- package/dist/storage/sqlite-adapter.js +84 -12
- package/dist/tools/delete-rule.d.ts +2 -1
- package/dist/tools/delete-rule.js +13 -4
- package/dist/tools/delete-trace.js +2 -1
- package/dist/tools/deploy-rule.d.ts +2 -1
- package/dist/tools/deploy-rule.js +29 -7
- package/dist/tools/evaluate-output.js +45 -11
- package/dist/tools/evaluate-with-llm-judge.js +3 -2
- package/dist/tools/get-traces.js +6 -2
- package/dist/tools/index.js +2 -2
- package/dist/tools/list-rules.js +2 -1
- package/dist/tools/log-trace.d.ts +51 -0
- package/dist/tools/log-trace.js +15 -3
- package/dist/tools/strict-input.d.ts +2 -0
- package/dist/tools/strict-input.js +35 -0
- package/dist/tools/verify-citations.js +8 -6
- package/dist/transport/http.js +24 -2
- package/dist/types/decision-moment.d.ts +20 -0
- package/dist/types/eval.d.ts +47 -0
- package/dist/types/query.d.ts +1 -1
- package/dist/utils/write-atomic.d.ts +2 -0
- package/dist/utils/write-atomic.js +34 -2
- package/package.json +3 -2
- package/server.json +3 -3
- package/dist/dashboard/assets/index-B4Aw6ozt.css +0 -1
- package/dist/dashboard/assets/index-ChcHJDDJ.js +0 -10
|
@@ -3,12 +3,14 @@ import * as migration002 from './002-eval-skip-fields.js';
|
|
|
3
3
|
import * as migration003 from './003-eval-passed-index.js';
|
|
4
4
|
import * as migration004 from './004-tenant-id.js';
|
|
5
5
|
import * as migration005 from './005-normalize-created-at.js';
|
|
6
|
+
import * as migration006 from './006-eval-critical-failures.js';
|
|
6
7
|
const migrations = [
|
|
7
8
|
migration001,
|
|
8
9
|
migration002,
|
|
9
10
|
migration003,
|
|
10
11
|
migration004,
|
|
11
12
|
migration005,
|
|
13
|
+
migration006,
|
|
12
14
|
];
|
|
13
15
|
export function runMigrations(db) {
|
|
14
16
|
db.exec(`
|
|
@@ -4,6 +4,7 @@ import type { EvalResult } from '../types/eval.js';
|
|
|
4
4
|
import type { TenantId } from '../types/tenant.js';
|
|
5
5
|
export declare class SqliteAdapter implements IStorageAdapter {
|
|
6
6
|
private db;
|
|
7
|
+
private readonly dbPath;
|
|
7
8
|
constructor(dbPath: string);
|
|
8
9
|
initialize(): Promise<void>;
|
|
9
10
|
close(): Promise<void>;
|
|
@@ -26,6 +27,7 @@ export declare class SqliteAdapter implements IStorageAdapter {
|
|
|
26
27
|
total: number;
|
|
27
28
|
}>;
|
|
28
29
|
getDashboardSummary(tenantId: TenantId, sinceHours?: number): Promise<DashboardSummary>;
|
|
30
|
+
private static readonly PERIOD_HOURS;
|
|
29
31
|
private periodToSince;
|
|
30
32
|
getEvalStats(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStats>;
|
|
31
33
|
getEvalStatsTrend(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStatsTrendBucket[]>;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* its own data.
|
|
21
21
|
*/
|
|
22
22
|
import Database from 'better-sqlite3';
|
|
23
|
+
import { ensureOwnerOnly } from '../utils/write-atomic.js';
|
|
23
24
|
import { TenantContextRequiredError } from '../types/tenant.js';
|
|
24
25
|
import { runMigrations } from './migrations/index.js';
|
|
25
26
|
const ALLOWED_SORT_COLUMNS = new Set(['timestamp', 'latency_ms', 'cost_usd']);
|
|
@@ -35,7 +36,9 @@ function assertTenant(tenantId) {
|
|
|
35
36
|
}
|
|
36
37
|
export class SqliteAdapter {
|
|
37
38
|
db;
|
|
39
|
+
dbPath;
|
|
38
40
|
constructor(dbPath) {
|
|
41
|
+
this.dbPath = dbPath;
|
|
39
42
|
this.db = new Database(dbPath);
|
|
40
43
|
}
|
|
41
44
|
async initialize() {
|
|
@@ -43,6 +46,17 @@ export class SqliteAdapter {
|
|
|
43
46
|
this.db.pragma('busy_timeout = 5000');
|
|
44
47
|
this.db.pragma('foreign_keys = ON');
|
|
45
48
|
runMigrations(this.db);
|
|
49
|
+
/*
|
|
50
|
+
* iris.db holds agent inputs and outputs verbatim, and a tool that
|
|
51
|
+
* detects PII necessarily stores the PII it found. better-sqlite3
|
|
52
|
+
* creates the file with the process umask (typically 0644 = readable by
|
|
53
|
+
* every local account), and WAL mode creates two sidecars that hold the
|
|
54
|
+
* same data. Narrow all three after the pragmas, since -wal/-shm do not
|
|
55
|
+
* exist until WAL is enabled. No-op on Windows and on :memory:.
|
|
56
|
+
*/
|
|
57
|
+
if (this.dbPath !== ':memory:') {
|
|
58
|
+
ensureOwnerOnly(this.dbPath, `${this.dbPath}-wal`, `${this.dbPath}-shm`);
|
|
59
|
+
}
|
|
46
60
|
}
|
|
47
61
|
async close() {
|
|
48
62
|
this.db.close();
|
|
@@ -97,13 +111,31 @@ export class SqliteAdapter {
|
|
|
97
111
|
conditions.push('timestamp <= ?');
|
|
98
112
|
params.push(filter.until);
|
|
99
113
|
}
|
|
100
|
-
if (filter?.min_score !== undefined) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
if (filter?.min_score !== undefined || filter?.max_score !== undefined) {
|
|
115
|
+
/*
|
|
116
|
+
* Both bounds apply to the LATEST eval per trace (created_at DESC,
|
|
117
|
+
* rowid breaking ties within the same millisecond) — the semantics
|
|
118
|
+
* the get_traces description promises. These used to be two
|
|
119
|
+
* INDEPENDENT EXISTS subqueries, so a trace with evals at 0.95 and
|
|
120
|
+
* 0.05 matched min_score=0.4 + max_score=0.6: each bound was
|
|
121
|
+
* satisfied by a different eval even though no single eval — let
|
|
122
|
+
* alone the latest — was in range (#332).
|
|
123
|
+
*/
|
|
124
|
+
const scoreBounds = [];
|
|
125
|
+
if (filter.min_score !== undefined) {
|
|
126
|
+
scoreBounds.push('e.score >= ?');
|
|
127
|
+
}
|
|
128
|
+
if (filter.max_score !== undefined) {
|
|
129
|
+
scoreBounds.push('e.score <= ?');
|
|
130
|
+
}
|
|
131
|
+
conditions.push('EXISTS (SELECT 1 FROM eval_results e WHERE e.rowid = ' +
|
|
132
|
+
'(SELECT e2.rowid FROM eval_results e2 WHERE e2.tenant_id = traces.tenant_id AND e2.trace_id = traces.trace_id ' +
|
|
133
|
+
'ORDER BY e2.created_at DESC, e2.rowid DESC LIMIT 1) ' +
|
|
134
|
+
`AND ${scoreBounds.join(' AND ')})`);
|
|
135
|
+
if (filter.min_score !== undefined)
|
|
136
|
+
params.push(filter.min_score);
|
|
137
|
+
if (filter.max_score !== undefined)
|
|
138
|
+
params.push(filter.max_score);
|
|
107
139
|
}
|
|
108
140
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
109
141
|
const sortBy = options.sort_by ?? 'timestamp';
|
|
@@ -153,10 +185,18 @@ export class SqliteAdapter {
|
|
|
153
185
|
* calendar date matched the boundary's date was dropped from the
|
|
154
186
|
* window. Migration 005 rewrites rows written before this line existed.
|
|
155
187
|
*/
|
|
188
|
+
/*
|
|
189
|
+
* critical_failures is PERSISTED (migration 006) because the veto is a
|
|
190
|
+
* verdict, not a presentation detail. It used to live only in the live
|
|
191
|
+
* tool response, so the moment an evaluation was stored a vetoed eval
|
|
192
|
+
* became indistinguishable from one that merely scored below threshold —
|
|
193
|
+
* no surface could filter, count, or explain the release's flagship
|
|
194
|
+
* behaviour. NULL when nothing vetoed.
|
|
195
|
+
*/
|
|
156
196
|
this.db.prepare(`
|
|
157
|
-
INSERT INTO eval_results (tenant_id, id, trace_id, eval_type, output_text, expected_text, score, passed, rule_results, suggestions, rules_evaluated, rules_skipped, insufficient_data, created_at)
|
|
158
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
159
|
-
`).run(tenantId, result.id, result.trace_id ?? null, result.eval_type, result.output_text, result.expected_text ?? null, result.score, result.passed ? 1 : 0, JSON.stringify(result.rule_results), JSON.stringify(result.suggestions), result.rules_evaluated ?? null, result.rules_skipped ?? null, result.insufficient_data ? 1 : 0, new Date().toISOString());
|
|
197
|
+
INSERT INTO eval_results (tenant_id, id, trace_id, eval_type, output_text, expected_text, score, passed, rule_results, suggestions, rules_evaluated, rules_skipped, insufficient_data, critical_failures, created_at)
|
|
198
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
199
|
+
`).run(tenantId, result.id, result.trace_id ?? null, result.eval_type, result.output_text, result.expected_text ?? null, result.score, result.passed ? 1 : 0, JSON.stringify(result.rule_results), JSON.stringify(result.suggestions), result.rules_evaluated ?? null, result.rules_skipped ?? null, result.insufficient_data ? 1 : 0, result.critical_failures?.length ? JSON.stringify(result.critical_failures) : null, new Date().toISOString());
|
|
160
200
|
}
|
|
161
201
|
async getEvalsByTraceId(tenantId, traceId) {
|
|
162
202
|
assertTenant(tenantId);
|
|
@@ -244,22 +284,46 @@ export class SqliteAdapter {
|
|
|
244
284
|
// ---------------------------------------------------------------------------
|
|
245
285
|
// Eval-stats endpoints (v0.2.0 dashboard)
|
|
246
286
|
// ---------------------------------------------------------------------------
|
|
287
|
+
/*
|
|
288
|
+
* Table-driven rather than a nested ternary: the old form silently fell
|
|
289
|
+
* through to 720 hours for anything that wasn't '24h' or '7d', so a new
|
|
290
|
+
* period value would have quietly returned 30d data rather than failing.
|
|
291
|
+
*/
|
|
292
|
+
static PERIOD_HOURS = {
|
|
293
|
+
'24h': 24,
|
|
294
|
+
'2d': 48,
|
|
295
|
+
'7d': 168,
|
|
296
|
+
'14d': 336,
|
|
297
|
+
'30d': 720,
|
|
298
|
+
'60d': 1440,
|
|
299
|
+
'90d': 2160,
|
|
300
|
+
'180d': 4320,
|
|
301
|
+
};
|
|
247
302
|
periodToSince(period) {
|
|
248
303
|
if (period === 'all')
|
|
249
304
|
return '1970-01-01T00:00:00.000Z';
|
|
250
|
-
const hours = period
|
|
305
|
+
const hours = SqliteAdapter.PERIOD_HOURS[period];
|
|
251
306
|
return new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
252
307
|
}
|
|
253
308
|
async getEvalStats(tenantId, period) {
|
|
254
309
|
assertTenant(tenantId);
|
|
255
310
|
const since = this.periodToSince(period);
|
|
311
|
+
/*
|
|
312
|
+
* No trace_id filter — deliberately. evaluate_output without a
|
|
313
|
+
* trace_id is documented and normal, and every sibling scan (trend,
|
|
314
|
+
* per-rule breakdown, failures) counts unlinked evals. Filtering only
|
|
315
|
+
* this headline made totalEvals disagree with the trend's sum, and —
|
|
316
|
+
* because eval_results.trace_id is ON DELETE SET NULL — deleting a
|
|
317
|
+
* trace retroactively shrank the headline while the trend kept the
|
|
318
|
+
* eval. One population everywhere: every eval in the window.
|
|
319
|
+
*/
|
|
256
320
|
const agg = this.db.prepare(`
|
|
257
321
|
SELECT
|
|
258
322
|
COUNT(*) AS total_evals,
|
|
259
323
|
COALESCE(AVG(score), 0) AS avg_score,
|
|
260
324
|
SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) AS passed_count
|
|
261
325
|
FROM eval_results
|
|
262
|
-
WHERE tenant_id = ? AND created_at >= ?
|
|
326
|
+
WHERE tenant_id = ? AND created_at >= ?
|
|
263
327
|
`).get(tenantId, since);
|
|
264
328
|
const cost = this.db.prepare(`
|
|
265
329
|
SELECT COALESCE(SUM(cost_usd), 0) AS total_cost
|
|
@@ -497,6 +561,14 @@ export class SqliteAdapter {
|
|
|
497
561
|
rules_evaluated: row.rules_evaluated,
|
|
498
562
|
rules_skipped: row.rules_skipped,
|
|
499
563
|
insufficient_data: row.insufficient_data != null ? row.insufficient_data === 1 : undefined,
|
|
564
|
+
/*
|
|
565
|
+
* Absent, not [], when NULL. Rows written before migration 006 never
|
|
566
|
+
* captured the field, and returning an empty array would assert "no
|
|
567
|
+
* critical rule failed" about an evaluation that never recorded one.
|
|
568
|
+
*/
|
|
569
|
+
...(row.critical_failures != null
|
|
570
|
+
? { critical_failures: JSON.parse(row.critical_failures) }
|
|
571
|
+
: {}),
|
|
500
572
|
};
|
|
501
573
|
}
|
|
502
574
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { CustomRuleStore } from '../custom-rule-store.js';
|
|
3
|
-
|
|
3
|
+
import type { EvalEngine } from '../eval/engine.js';
|
|
4
|
+
export declare function registerDeleteRuleTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* delete_rule MCP tool — remove a deployed custom rule.
|
|
3
3
|
*
|
|
4
4
|
* Destructive counterpart to deploy_rule. Removes the rule from
|
|
5
|
-
* ~/.iris/custom-rules.json
|
|
6
|
-
*
|
|
5
|
+
* ~/.iris/custom-rules.json AND unregisters it from the live eval
|
|
6
|
+
* engine, so it stops firing on the very next evaluate_output call —
|
|
7
|
+
* no restart needed. Appends a `rule.delete` entry to the audit log.
|
|
7
8
|
*
|
|
8
9
|
* Past eval_results that referenced this rule stay intact — the
|
|
9
10
|
* history is preserved even after the rule is removed. The audit
|
|
@@ -11,13 +12,14 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { z } from 'zod';
|
|
13
14
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
15
|
+
import { strictInput } from './strict-input.js';
|
|
14
16
|
const inputSchema = {
|
|
15
17
|
rule_id: z
|
|
16
18
|
.string()
|
|
17
19
|
.regex(/^rule-[a-z0-9]+$/)
|
|
18
20
|
.describe('Rule id to delete (format: rule-<hex>); obtained from list_rules or deploy_rule response'),
|
|
19
21
|
};
|
|
20
|
-
export function registerDeleteRuleTool(server, customRuleStore) {
|
|
22
|
+
export function registerDeleteRuleTool(server, customRuleStore, evalEngine) {
|
|
21
23
|
server.registerTool('delete_rule', {
|
|
22
24
|
title: 'Delete Custom Rule',
|
|
23
25
|
description: [
|
|
@@ -37,7 +39,7 @@ export function registerDeleteRuleTool(server, customRuleStore) {
|
|
|
37
39
|
'',
|
|
38
40
|
"Error modes. Throws 400 on malformed rule_id (wrong prefix). Returns `{deleted: false}` if rule_id doesn't match any deployed rule (not an error — idempotent-ish). Returns 429 on HTTP rate limit. File-write failures propagate as 500.",
|
|
39
41
|
].join('\n'),
|
|
40
|
-
inputSchema,
|
|
42
|
+
inputSchema: strictInput(inputSchema),
|
|
41
43
|
annotations: {
|
|
42
44
|
readOnlyHint: false,
|
|
43
45
|
destructiveHint: true,
|
|
@@ -47,6 +49,13 @@ export function registerDeleteRuleTool(server, customRuleStore) {
|
|
|
47
49
|
}, async (args) => {
|
|
48
50
|
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
49
51
|
const deleted = customRuleStore.delete(LOCAL_TENANT, args.rule_id, 'mcp');
|
|
52
|
+
if (deleted) {
|
|
53
|
+
// Hot-remove from the live engine so the rule stops firing on the
|
|
54
|
+
// very next evaluate_output call — the "stops firing immediately on
|
|
55
|
+
// the live process" this description promises (#332). No-op when the
|
|
56
|
+
// rule was never registered in this process.
|
|
57
|
+
evalEngine.unregisterRule(args.rule_id);
|
|
58
|
+
}
|
|
50
59
|
return {
|
|
51
60
|
content: [
|
|
52
61
|
{
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
14
|
+
import { strictInput } from './strict-input.js';
|
|
14
15
|
const inputSchema = {
|
|
15
16
|
trace_id: z
|
|
16
17
|
.string()
|
|
@@ -37,7 +38,7 @@ export function registerDeleteTraceTool(server, storage) {
|
|
|
37
38
|
'',
|
|
38
39
|
"Error modes. Throws 400 on malformed trace_id (wrong format: not 32-char lowercase hex). Returns `{deleted: false}` when the id doesn't exist in the caller's tenant (not an error — the trace may simply have been deleted already). Returns 429 on HTTP rate limit. Storage failures propagate as 500.",
|
|
39
40
|
].join('\n'),
|
|
40
|
-
inputSchema,
|
|
41
|
+
inputSchema: strictInput(inputSchema),
|
|
41
42
|
annotations: {
|
|
42
43
|
readOnlyHint: false,
|
|
43
44
|
destructiveHint: true,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { CustomRuleStore } from '../custom-rule-store.js';
|
|
3
|
-
|
|
3
|
+
import type { EvalEngine } from '../eval/engine.js';
|
|
4
|
+
export declare function registerDeployRuleTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* eval_type.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
+
import { createCustomRule } from '../eval/rules/custom.js';
|
|
14
15
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
16
|
+
import { strictInput } from './strict-input.js';
|
|
15
17
|
const CustomRuleDefinitionSchema = z.object({
|
|
16
18
|
name: z.string(),
|
|
17
19
|
type: z.enum([
|
|
@@ -28,7 +30,11 @@ const CustomRuleDefinitionSchema = z.object({
|
|
|
28
30
|
weight: z.number().optional(),
|
|
29
31
|
});
|
|
30
32
|
const inputSchema = {
|
|
31
|
-
|
|
33
|
+
// 80 mirrors the persisted store's cap (custom-rule-store.ts). The tool
|
|
34
|
+
// used to allow 120, so a 100-char name passed the tool schema and then
|
|
35
|
+
// surfaced the store's ZodError as a raw 500 (#332). One limit, enforced
|
|
36
|
+
// at the boundary, fails cleanly as a 400.
|
|
37
|
+
name: z.string().min(1).max(80).describe('Human-readable rule name (1-80 chars; used in eval results)'),
|
|
32
38
|
description: z
|
|
33
39
|
.string()
|
|
34
40
|
.max(500)
|
|
@@ -40,14 +46,14 @@ const inputSchema = {
|
|
|
40
46
|
severity: z
|
|
41
47
|
.enum(['low', 'medium', 'high', 'critical'])
|
|
42
48
|
.default('medium')
|
|
43
|
-
.describe('
|
|
49
|
+
.describe('What a FAILURE of this rule means. low/medium: informational — contributes to the weighted score only (plus dashboard sort + audit alerts). high/critical: hard-fail — a failing evaluation of this rule forces the overall passed=false regardless of the weighted score'),
|
|
44
50
|
definition: CustomRuleDefinitionSchema.describe('Check definition (regex, length, keyword, cost, or schema)'),
|
|
45
51
|
sourceMomentId: z
|
|
46
52
|
.string()
|
|
47
53
|
.optional()
|
|
48
54
|
.describe('Optional Decision Moment ID the rule was derived from (preserves workflow-inversion provenance)'),
|
|
49
55
|
};
|
|
50
|
-
export function registerDeployRuleTool(server, customRuleStore) {
|
|
56
|
+
export function registerDeployRuleTool(server, customRuleStore, evalEngine) {
|
|
51
57
|
server.registerTool('deploy_rule', {
|
|
52
58
|
title: 'Deploy Custom Rule',
|
|
53
59
|
description: [
|
|
@@ -63,11 +69,11 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
63
69
|
'',
|
|
64
70
|
"Don't use to VALIDATE a rule before committing — deploy writes immediately. Use the dashboard's preview endpoint (POST /api/v1/rules/custom/preview) for dry-run validation against sample output. Don't use to EDIT an existing rule — this call only creates; edits require a dedicated flow (coming in v0.5). To update a rule today: delete_rule then deploy_rule with the new definition.",
|
|
65
71
|
'',
|
|
66
|
-
'Parameters. name is 1-
|
|
72
|
+
'Parameters. name is 1-80 chars (Zod-enforced min/max — the same cap the persisted store applies); appears in eval_result rule_results so make it human-readable. description is optional, max 500 chars (used in dashboard tooltips). evalType determines WHEN the rule fires (must match the eval_type your evaluate_output calls use; e.g., a "completeness" rule fires on every evaluate_output where eval_type="completeness" OR eval_type="custom"). severity decides what a FAILURE of the rule does: low/medium failures only lower the weighted score (and drive dashboard sort + audit alerts); high/critical failures HARD-FAIL the evaluation — the overall `passed` is forced to false regardless of the weighted score, and the rule is listed in the response\'s `critical_failures`. Severity never changes the numeric score itself (that uses the rule\'s weight). definition.type and definition.config must match (e.g., regex_match needs config.pattern; cost_threshold needs config.max_cost; min_length needs config.min_length; max_length needs config.max_length; contains_keywords/excludes_keywords need config.keywords). Invalid configs are now REJECTED at deploy time with the offending field named, instead of deploying and then failing every evaluation. sourceMomentId is optional but recommended (preserves workflow-inversion provenance from Make-This-A-Rule composer). Defaults: severity="medium".',
|
|
67
73
|
'',
|
|
68
|
-
"Error modes. Throws 400 on invalid definition (Zod rejects — e.g., regex that fails safe-regex2 ReDoS check, or length > 1000 chars). Throws 400 on empty `name
|
|
74
|
+
"Error modes. Throws 400 on invalid definition (Zod rejects — e.g., regex that fails safe-regex2 ReDoS check, or length > 1000 chars). Throws 400 on empty `name` or `name` over 80 chars. Any evalType/definition.type combination is valid (a regex_match rule can enforce a safety policy; a max_length rule can express completeness) — there is no category/type mismatch error. Returns 429 when HTTP rate limit exceeded. File-write failures (disk full, read-only fs) propagate as 500; the audit log is best-effort and does not block deploy.",
|
|
69
75
|
].join('\n'),
|
|
70
|
-
inputSchema,
|
|
76
|
+
inputSchema: strictInput(inputSchema),
|
|
71
77
|
annotations: {
|
|
72
78
|
readOnlyHint: false,
|
|
73
79
|
destructiveHint: false,
|
|
@@ -75,16 +81,32 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
75
81
|
openWorldHint: false,
|
|
76
82
|
},
|
|
77
83
|
}, async (args) => {
|
|
84
|
+
// Server overrides the inner definition's `name` so it always matches
|
|
85
|
+
// the user-facing rule name — same normalization the dashboard's
|
|
86
|
+
// deploy route applies. Also keeps the tool's 80-char cap authoritative
|
|
87
|
+
// (an unchecked definition.name used to reach the store and surface its
|
|
88
|
+
// ZodError as a raw 500).
|
|
89
|
+
const definition = {
|
|
90
|
+
...args.definition,
|
|
91
|
+
name: args.name,
|
|
92
|
+
};
|
|
78
93
|
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
79
94
|
const rule = customRuleStore.deploy(LOCAL_TENANT, {
|
|
80
95
|
name: args.name,
|
|
81
96
|
description: args.description,
|
|
82
97
|
evalType: args.evalType,
|
|
83
98
|
severity: args.severity,
|
|
84
|
-
definition
|
|
99
|
+
definition,
|
|
85
100
|
sourceMomentId: args.sourceMomentId,
|
|
86
101
|
user: 'mcp',
|
|
87
102
|
});
|
|
103
|
+
// Register with the live engine so the rule fires on the very next
|
|
104
|
+
// evaluate_output call — the "activates immediately for the running
|
|
105
|
+
// process" this description promises. Previously only the dashboard's
|
|
106
|
+
// deploy route did this; MCP deploys silently waited for a restart.
|
|
107
|
+
// Registered under its rule id so delete_rule can hot-remove it.
|
|
108
|
+
// Severity rides along: high/critical makes the rule hard-failing.
|
|
109
|
+
evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition, rule.severity), rule.id);
|
|
88
110
|
return {
|
|
89
111
|
content: [
|
|
90
112
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { strictInput } from './strict-input.js';
|
|
3
4
|
const CustomRuleSchema = z.object({
|
|
4
5
|
name: z.string(),
|
|
5
6
|
type: z.enum([
|
|
@@ -11,12 +12,21 @@ const CustomRuleSchema = z.object({
|
|
|
11
12
|
});
|
|
12
13
|
const inputSchema = {
|
|
13
14
|
output: z.string().describe('The output text to evaluate (the agent\'s response that gets scored against rules)'),
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// .optional() rather than .default('completeness') so the handler can tell
|
|
16
|
+
// "caller chose completeness" apart from "caller never chose" — the second
|
|
17
|
+
// case gets a note in the response saying safety rules did not run. The
|
|
18
|
+
// effective default is still completeness.
|
|
19
|
+
eval_type: z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']).optional().describe('Rule bundle to apply: completeness | relevance | safety | cost | custom — picks which built-in rules fire. Defaults to "completeness" when omitted (the response then carries a note that safety rules did not run)'),
|
|
20
|
+
expected: z.string().optional().describe('Expected output for comparison — consulted only by the completeness bundle\'s expected_coverage rule; NOT used by relevance (the relevance rules compare the output against `input`)'),
|
|
21
|
+
input: z.string().optional().describe('Original input for context (the ask + any source material the agent was given) — REQUIRED when eval_type="relevance" (keyword_overlap and topic_consistency compare the output against it and skip without it); also grounds the safety bundle\'s hallucination signals'),
|
|
17
22
|
trace_id: z.string().optional().describe('Link evaluation to a trace — surfaces this eval in the dashboard\'s trace drill-through'),
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
// .max(10): inline rules skip the deploy-time probe, and the engine runs
|
|
24
|
+
// rules synchronously — without a cap, one request carrying N sandbox-
|
|
25
|
+
// defeating regex rules stalls the server linearly in N (measured 9.3s at
|
|
26
|
+
// N=50). Ten is ample for per-call rules; persistent sets belong in
|
|
27
|
+
// deploy_rule, where deploy-time validation probes each pattern.
|
|
28
|
+
custom_rules: z.array(CustomRuleSchema).max(10).optional().describe('Custom evaluation rules, max 10 per call (deploy persistent rule sets via deploy_rule instead) — fires REGARDLESS of eval_type; pass eval_type="custom" if you want ONLY these'),
|
|
29
|
+
cost_usd: z.number().optional().describe('Cost in USD — consulted by the cost bundle (eval_type="cost") AND by any cost_threshold custom rule regardless of eval_type; omit it and such a rule skips rather than passes (a critical one is listed in critical_skipped)'),
|
|
20
30
|
token_usage: z.object({
|
|
21
31
|
prompt_tokens: z.number().optional(),
|
|
22
32
|
completion_tokens: z.number().optional(),
|
|
@@ -33,17 +43,19 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
33
43
|
'',
|
|
34
44
|
'Behavior. Deterministic, in-process scoring — same inputs always produce the same result. Writes one eval_result row to Iris storage (linked to trace_id if provided; unlinked otherwise). No external network calls in heuristic mode (v0.4 adds an llm_as_judge eval_type that DOES call LLM APIs; see the separate evaluate_with_llm_judge tool for that). Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio. Runs in ~5-50ms for rule-based evaluation.',
|
|
35
45
|
'',
|
|
36
|
-
'Output shape. Returns JSON: `{ "id": "<uuid>", "score": 0..1, "passed": boolean, "rule_results": [{ "ruleName", "passed", "score", "message", "skipped?" }], "suggestions": string[], "rules_evaluated": number, "rules_skipped": number, "insufficient_data": boolean }`. `insufficient_data=true` means no applicable rules fired (e.g., safety eval with only cost data).',
|
|
46
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "eval_type": "<bundle that ran>", "score": 0..1, "passed": boolean, "critical_failures?": string[], "critical_skipped?": string[], "rule_results": [{ "ruleName", "passed", "score", "message", "skipped?", "skipReason?", "budgetExceeded?", "configInvalid?" }], "suggestions": string[], "rules_evaluated": number, "rules_skipped": number, "insufficient_data": boolean, "note?": string }`. `insufficient_data=true` means no applicable rules fired (e.g., safety eval with only cost data). `note` appears only when eval_type was omitted, naming the defaulted bundle and that safety rules did not run.',
|
|
37
47
|
'',
|
|
38
|
-
'
|
|
48
|
+
'What `passed` means. `score` and `passed` answer different questions. `score` is the weighted average across the rules that ran — a 0..1 quality gradient. `passed` is the ship/no-ship verdict: true only when the score clears the pass threshold (default 0.7, configurable via config `eval.defaultThreshold`) AND no critical rule failed. Critical rules HARD-FAIL: if one fails, `passed` is false regardless of the weighted score, and the culprits are listed in `critical_failures`. The critical rules are the genuine safety violations — `no_pii`, `no_injection_patterns`, `no_blocklist_words` — plus any deployed custom rule with severity high/critical. A leaked SSN can never be averaged away by other rules passing. One caveat, stated because it is reachable on purpose: a critical rule that SKIPPED did not judge the output and therefore cannot veto — a regex rule whose match blew the 100ms sandbox budget on crafted output skips, so `passed` can be true with no `critical_failures`. Every such rule is named in `critical_skipped`. If your gate must fail closed, treat a non-empty `critical_skipped` as UNKNOWN, not clean.',
|
|
49
|
+
'',
|
|
50
|
+
'Use when you want a quality score on a specific output — typically after log_trace records the execution. Pass `eval_type` to route to the right rule bundle: `completeness` (length, non-empty output, sentence count, coverage of `expected`), `relevance` (keyword overlap and topic consistency against `input`), `safety` (PII leak, prompt injection, hallucination markers, stub-output detection — pass `input` so the hallucination signals can cross-check the output against the material the agent was given), `cost` (budget threshold), or `custom` (bring your own rules via `custom_rules`).',
|
|
39
51
|
'',
|
|
40
52
|
'Don\'t use when the output is empty or has no applicable rules — the eval_type decides which rules apply, and invalid combinations return score=0 + insufficient_data=true (not an error, but not actionable). Don\'t use to VALIDATE JSON schemas directly (use your language\'s JSON Schema validator — Iris\'s `json_schema` custom rule type is for output-shape assertions, not arbitrary validation).',
|
|
41
53
|
'',
|
|
42
|
-
'Parameters.
|
|
54
|
+
'Parameters. input is REQUIRED when eval_type="relevance" (keyword_overlap and topic_consistency compare the output against it; without it both rules skip and the response reports insufficient_data=true) AND grounds the safety bundle\'s hallucination signals (without it those signals stay silent rather than guess); ignored otherwise. expected is consulted only by the completeness bundle\'s expected_coverage rule; ignored for other eval_types — it is NOT the relevance target. cost_usd is consulted by the cost bundle AND by any cost_threshold custom rule regardless of eval_type — omit it and such a rule skips rather than passes (a critical one is listed in critical_skipped); token_usage is ONLY consulted when eval_type="cost". custom_rules ALWAYS fires regardless of eval_type — pass eval_type="custom" if you want ONLY your rules to run (otherwise both your rules AND the eval_type bundle run together). trace_id is optional but recommended (linking the eval to its trace surfaces it in the dashboard\'s drill-through). Defaults: eval_type="completeness" — and when you rely on that default, the response carries a `note` reminding you that the safety bundle did not run.',
|
|
43
55
|
'',
|
|
44
|
-
'Error modes. Throws on malformed custom_rules (Zod rejects).
|
|
56
|
+
'Error modes. Throws on unknown argument names (strict schema — a misspelled argument is rejected with the valid argument list, never silently dropped). Throws on malformed custom_rules (Zod rejects the shape: missing name/type, unknown type, non-object config) and on more than 10 custom_rules in one call (use deploy_rule for persistent rule sets). An inline rule whose CONFIG is unusable — a regex that fails the safe-regex2 ReDoS check or exceeds the 1000-char limit, a missing or non-string config.pattern, non-string keywords — does NOT error: that rule reports skipped with configInvalid=true and a skipReason naming the field, and the other rules still run (deploy_rule rejects the same configs with a 400 at deploy time). Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. The eval itself never throws — failing rules report `passed: false` with a message, they don\'t bubble exceptions. A regex that exceeds the 100ms sandbox matching budget on a given output reports skipped with budgetExceeded=true instead of hanging the server (fail-open per rule — gate on that flag if you must fail closed).',
|
|
45
57
|
].join('\n'),
|
|
46
|
-
inputSchema,
|
|
58
|
+
inputSchema: strictInput(inputSchema),
|
|
47
59
|
annotations: {
|
|
48
60
|
readOnlyHint: false, // Writes an eval_result row
|
|
49
61
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
@@ -51,7 +63,12 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
51
63
|
openWorldHint: false, // No external network in heuristic mode; LLM-as-judge has its own tool with openWorldHint:true
|
|
52
64
|
},
|
|
53
65
|
}, async (args) => {
|
|
54
|
-
|
|
66
|
+
// Track omission explicitly: a caller who never chose a bundle gets
|
|
67
|
+
// the completeness default AND a note saying so — six of seven UAT
|
|
68
|
+
// personas read passed:true on PII-laden text with no hint that the
|
|
69
|
+
// safety bundle never ran.
|
|
70
|
+
const evalTypeOmitted = args.eval_type === undefined;
|
|
71
|
+
const evalType = (args.eval_type ?? 'completeness');
|
|
55
72
|
const result = evalEngine.evaluate(evalType, {
|
|
56
73
|
output: args.output,
|
|
57
74
|
expected: args.expected,
|
|
@@ -71,13 +88,30 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
71
88
|
type: 'text',
|
|
72
89
|
text: JSON.stringify({
|
|
73
90
|
id: result.id,
|
|
91
|
+
// Echo which bundle actually ran. Without this, a caller who
|
|
92
|
+
// omitted eval_type could not tell a "safety pass" from a
|
|
93
|
+
// completeness eval that never ran a single safety rule.
|
|
94
|
+
eval_type: result.eval_type,
|
|
74
95
|
score: result.score,
|
|
75
96
|
passed: result.passed,
|
|
97
|
+
...(result.critical_failures ? { critical_failures: result.critical_failures } : {}),
|
|
98
|
+
// The other half of the veto contract. The engine names every
|
|
99
|
+
// critical rule that SKIPPED (budget-killed regex, missing cost
|
|
100
|
+
// data) so a fail-closed gate can treat the eval as unknown;
|
|
101
|
+
// this response used to drop the field, so the gate the
|
|
102
|
+
// description tells users to write keyed on something that
|
|
103
|
+
// never arrived and read passed:true as clean.
|
|
104
|
+
...(result.critical_skipped ? { critical_skipped: result.critical_skipped } : {}),
|
|
76
105
|
rule_results: result.rule_results,
|
|
77
106
|
suggestions: result.suggestions,
|
|
78
107
|
rules_evaluated: result.rules_evaluated,
|
|
79
108
|
rules_skipped: result.rules_skipped,
|
|
80
109
|
insufficient_data: result.insufficient_data,
|
|
110
|
+
...(evalTypeOmitted
|
|
111
|
+
? {
|
|
112
|
+
note: 'eval_type was omitted, so the default "completeness" bundle ran. Safety rules (PII, injection, blocklist, stub, hallucination) were NOT part of this evaluation — pass eval_type="safety" to run them.',
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
81
115
|
}),
|
|
82
116
|
},
|
|
83
117
|
],
|
|
@@ -3,6 +3,7 @@ import { LOCAL_TENANT } from '../types/tenant.js';
|
|
|
3
3
|
import { evaluateWithLLMJudge } from '../eval/llm-judge/evaluator.js';
|
|
4
4
|
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
5
|
import { generateEvalId } from '../utils/ids.js';
|
|
6
|
+
import { strictInput } from './strict-input.js';
|
|
6
7
|
const inputSchema = {
|
|
7
8
|
output: z.string().min(1).describe('The agent output text to evaluate'),
|
|
8
9
|
template: z
|
|
@@ -69,11 +70,11 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
69
70
|
'',
|
|
70
71
|
"Don't use for simple regex/length/keyword checks (use evaluate_output with heuristic rules — they're free, deterministic, 1000x faster). Don't use without an API key set (IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY). Don't use on very large outputs (>8K tokens) without raising max_cost_usd — the pre-check will refuse the call.",
|
|
71
72
|
'',
|
|
72
|
-
'Parameters. model is required (no default — pick consciously since cost varies 100x across models). provider is auto-detected from the model name; override only for ambiguous IDs. expected is REQUIRED when template="correctness" (the reference answer to compare against); ignored for other templates. source_material is REQUIRED when template="faithfulness" (the RAG sources to ground against); ignored otherwise. input is optional but improves scoring on helpfulness/safety templates (gives the judge the user prompt that produced the output). max_cost_usd defaults to env var IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or $0.25 — the worst-case cost is computed BEFORE the call (input_tokens × prompt_price + max_output_tokens × completion_price); call refused upfront if
|
|
73
|
+
'Parameters. model is required (no default — pick consciously since cost varies 100x across models). provider is auto-detected from the model name; override only for ambiguous IDs. expected is REQUIRED when template="correctness" (the reference answer to compare against); ignored for other templates. source_material is REQUIRED when template="faithfulness" (the RAG sources to ground against); ignored otherwise. input is optional but improves scoring on helpfulness/safety templates (gives the judge the user prompt that produced the output). max_cost_usd defaults to env var IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or $0.25 — the worst-case cost is computed BEFORE the call (input_tokens × prompt_price + max_output_tokens × completion_price, PLUS the same for the one retry that fires if the judge\'s first reply is not valid JSON); call refused upfront if that two-attempt worst case would exceed. When a retry does run, the reported input_tokens / output_tokens / cost_usd / latency_ms are totals across both attempts. max_output_tokens caps the judge response (default 512, max 4096); higher = more rationale detail + more cost. temperature default 0 (deterministic). timeout_ms default 60000. trace_id optional but recommended (links eval to trace in dashboard). Defaults: temperature=0, max_output_tokens=512, max_cost_usd=$0.25, timeout_ms=60000.',
|
|
73
74
|
'',
|
|
74
75
|
'Error modes. Throws when the required API key env var is missing. Throws when the estimated worst-case cost exceeds max_cost_usd (raise the cap or trim prompts). Throws LLMJudgeError on provider errors — kind=`auth` on 401/403, `rate_limit` on 429 (auto-retried once), `server_error` on 5xx, `timeout` on abort, `malformed_response` when the judge fails to emit valid JSON on both attempts. Throws "Unknown model" for unsupported model IDs — update src/eval/llm-judge/pricing.ts first.',
|
|
75
76
|
].join('\n'),
|
|
76
|
-
inputSchema,
|
|
77
|
+
inputSchema: strictInput(inputSchema),
|
|
77
78
|
annotations: {
|
|
78
79
|
readOnlyHint: false, // Writes eval_result; also spends money (external API cost)
|
|
79
80
|
destructiveHint: false, // Creates data; doesn't overwrite or delete
|
package/dist/tools/get-traces.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { strictInput } from './strict-input.js';
|
|
3
4
|
const inputSchema = {
|
|
4
5
|
agent_name: z.string().optional().describe('Filter by agent name — exact match (no wildcards in v0.4)'),
|
|
5
6
|
framework: z.string().optional().describe('Filter by agent framework — exact match (e.g., langchain, autogen)'),
|
|
@@ -7,7 +8,10 @@ const inputSchema = {
|
|
|
7
8
|
until: z.string().optional().describe('ISO timestamp upper bound — return traces with timestamp < this'),
|
|
8
9
|
min_score: z.number().optional().describe('Minimum eval score filter (0..1) — applied to LATEST eval per trace, not all evals'),
|
|
9
10
|
max_score: z.number().optional().describe('Maximum eval score filter (0..1) — applied to LATEST eval per trace'),
|
|
10
|
-
|
|
11
|
+
// Mirrors traceQuerySchema in dashboard/validation.ts — both capture paths
|
|
12
|
+
// (MCP tool, HTTP query) enforce the same 1..1000 bound. Unclamped, limit:-1
|
|
13
|
+
// meant "LIMIT -1" in SQLite, i.e. every row (#332).
|
|
14
|
+
limit: z.number().int().min(1).max(1000).default(50).describe('Results per page (default 50, max 1000 — values >1000 return 400)'),
|
|
11
15
|
offset: z.number().default(0).describe('Zero-based pagination offset — skip first N results'),
|
|
12
16
|
sort_by: z.enum(['timestamp', 'latency_ms', 'cost_usd']).default('timestamp').describe('Sort by timestamp | latency_ms | cost_usd (default timestamp)'),
|
|
13
17
|
sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order: asc | desc (default desc — most recent / highest first)'),
|
|
@@ -33,7 +37,7 @@ export function registerGetTracesTool(server, storage) {
|
|
|
33
37
|
'',
|
|
34
38
|
'Error modes. Returns 400 on invalid sort_by / sort_order (Zod enum). Returns 400 if limit > 1000. Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. Empty result with `total: 0` on no matches (not an error).',
|
|
35
39
|
].join('\n'),
|
|
36
|
-
inputSchema,
|
|
40
|
+
inputSchema: strictInput(inputSchema),
|
|
37
41
|
annotations: {
|
|
38
42
|
readOnlyHint: true, // Pure query: never writes, never deletes
|
|
39
43
|
destructiveHint: false, // Inverse of readOnly — trivially false
|
package/dist/tools/index.js
CHANGED
|
@@ -12,8 +12,8 @@ export function registerAllTools(server, storage, evalEngine, customRuleStore) {
|
|
|
12
12
|
registerEvaluateOutputTool(server, storage, evalEngine);
|
|
13
13
|
registerGetTracesTool(server, storage);
|
|
14
14
|
registerListRulesTool(server, customRuleStore);
|
|
15
|
-
registerDeployRuleTool(server, customRuleStore);
|
|
16
|
-
registerDeleteRuleTool(server, customRuleStore);
|
|
15
|
+
registerDeployRuleTool(server, customRuleStore, evalEngine);
|
|
16
|
+
registerDeleteRuleTool(server, customRuleStore, evalEngine);
|
|
17
17
|
registerDeleteTraceTool(server, storage);
|
|
18
18
|
registerEvaluateWithLLMJudgeTool(server, storage);
|
|
19
19
|
registerVerifyCitationsTool(server, storage);
|
package/dist/tools/list-rules.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
15
|
+
import { strictInput } from './strict-input.js';
|
|
15
16
|
const inputSchema = {
|
|
16
17
|
eval_type: z
|
|
17
18
|
.enum(['completeness', 'relevance', 'safety', 'cost', 'custom'])
|
|
@@ -42,7 +43,7 @@ export function registerListRulesTool(server, customRuleStore) {
|
|
|
42
43
|
'',
|
|
43
44
|
"Error modes. Returns empty list if the rule store file doesn't exist (first run). Returns 429 if HTTP rate limit exceeded. Never throws on valid input.",
|
|
44
45
|
].join('\n'),
|
|
45
|
-
inputSchema,
|
|
46
|
+
inputSchema: strictInput(inputSchema),
|
|
46
47
|
annotations: {
|
|
47
48
|
readOnlyHint: true,
|
|
48
49
|
destructiveHint: false,
|