@iris-eval/mcp-server 0.8.2 → 0.10.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/README.md +9 -2
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +17 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/capabilities.d.ts +3 -0
- package/dist/dashboard/routes/capabilities.js +11 -0
- package/dist/dashboard/routes/health.d.ts +5 -1
- package/dist/dashboard/routes/health.js +15 -3
- package/dist/dashboard/routes/rules.js +4 -1
- package/dist/dashboard/routes/traces.d.ts +3 -0
- package/dist/dashboard/routes/traces.js +11 -30
- package/dist/dashboard/seed-demo-data.js +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +6 -2
- package/dist/eval/accuracy.d.ts +41 -0
- package/dist/eval/accuracy.js +97 -0
- package/dist/eval/citation-verify/verifier.d.ts +16 -1
- package/dist/eval/citation-verify/verifier.js +14 -4
- package/dist/eval/compose.d.ts +57 -0
- package/dist/eval/compose.js +179 -0
- package/dist/eval/criticality.d.ts +15 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +6 -2
- package/dist/eval/engine.js +126 -12
- package/dist/eval/failure-classes.d.ts +8 -0
- package/dist/eval/failure-classes.js +18 -0
- package/dist/eval/llm-judge/evaluator.d.ts +30 -0
- package/dist/eval/llm-judge/evaluator.js +26 -2
- package/dist/eval/published-accuracy.d.ts +230 -0
- package/dist/eval/published-accuracy.js +86 -0
- package/dist/eval/questions.d.ts +12 -0
- package/dist/eval/questions.js +14 -0
- package/dist/eval/response-schema.d.ts +652 -0
- package/dist/eval/response-schema.js +130 -0
- package/dist/eval/response.d.ts +12 -0
- package/dist/eval/response.js +30 -0
- package/dist/eval/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +36 -1
- package/dist/eval/rules/cost.d.ts +2 -2
- package/dist/eval/rules/cost.js +50 -6
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +22 -0
- package/dist/eval/rules/relevance.js +23 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +224 -51
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +89 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -0
- package/dist/eval/text/checksums.d.ts +23 -0
- package/dist/eval/text/checksums.js +97 -0
- package/dist/eval/text/normalise.d.ts +30 -0
- package/dist/eval/text/normalise.js +265 -0
- package/dist/eval/text/sentences.d.ts +15 -0
- package/dist/eval/text/sentences.js +149 -0
- package/dist/eval/verdict.d.ts +34 -0
- package/dist/eval/verdict.js +131 -0
- package/dist/index.js +5 -28
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +53 -0
- package/dist/judge-enablement.d.ts +34 -0
- package/dist/judge-enablement.js +78 -0
- package/dist/judge-enablement.json +10 -0
- package/dist/preferences.d.ts +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +29 -0
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.js +65 -5
- package/dist/resources/uris.d.ts +12 -0
- package/dist/resources/uris.js +24 -0
- package/dist/retention.d.ts +20 -0
- package/dist/retention.js +44 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +17 -3
- package/dist/server.d.ts +10 -1
- package/dist/server.js +34 -7
- package/dist/storage/index.js +1 -1
- package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
- package/dist/storage/migrations/007-eval-provenance.js +30 -0
- package/dist/storage/migrations/index.js +24 -4
- package/dist/storage/sqlite-adapter.d.ts +26 -1
- package/dist/storage/sqlite-adapter.js +149 -15
- package/dist/tools/delete-rule.d.ts +8 -0
- package/dist/tools/delete-rule.js +30 -38
- package/dist/tools/delete-trace.d.ts +5 -0
- package/dist/tools/delete-trace.js +24 -27
- package/dist/tools/deploy-rule.d.ts +13 -1
- package/dist/tools/deploy-rule.js +37 -34
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +36 -0
- package/dist/tools/errors.d.ts +36 -0
- package/dist/tools/errors.js +134 -0
- package/dist/tools/evaluate-output.d.ts +8 -1
- package/dist/tools/evaluate-output.js +39 -60
- package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
- package/dist/tools/evaluate-with-llm-judge.js +124 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +29 -28
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +22 -1
- package/dist/tools/list-rules.d.ts +13 -0
- package/dist/tools/list-rules.js +43 -46
- package/dist/tools/log-trace.d.ts +4 -0
- package/dist/tools/log-trace.js +31 -29
- package/dist/tools/respond.d.ts +42 -0
- package/dist/tools/respond.js +90 -0
- package/dist/tools/strict-input.js +1 -1
- package/dist/tools/trace-link.d.ts +2 -0
- package/dist/tools/trace-link.js +13 -2
- package/dist/tools/verify-citations.d.ts +18 -2
- package/dist/tools/verify-citations.js +122 -96
- package/dist/types/config.d.ts +44 -0
- package/dist/types/eval.d.ts +309 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/resources/dashboard-summary.d.ts +0 -3
- package/dist/resources/dashboard-summary.js +0 -16
- package/dist/resources/trace-detail.d.ts +0 -3
- package/dist/resources/trace-detail.js +0 -30
|
@@ -4,6 +4,8 @@ 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
6
|
import * as migration006 from './006-eval-critical-failures.js';
|
|
7
|
+
import * as migration007 from './007-eval-provenance.js';
|
|
8
|
+
import { PKG_VERSION } from '../../config/defaults.js';
|
|
7
9
|
const migrations = [
|
|
8
10
|
migration001,
|
|
9
11
|
migration002,
|
|
@@ -11,6 +13,7 @@ const migrations = [
|
|
|
11
13
|
migration004,
|
|
12
14
|
migration005,
|
|
13
15
|
migration006,
|
|
16
|
+
migration007,
|
|
14
17
|
];
|
|
15
18
|
export function runMigrations(db) {
|
|
16
19
|
db.exec(`
|
|
@@ -19,10 +22,24 @@ export function runMigrations(db) {
|
|
|
19
22
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
20
23
|
)
|
|
21
24
|
`);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
25
|
+
const known = new Set(migrations.map((m) => m.id));
|
|
26
|
+
const hasWriterVersion = db.prepare("PRAGMA table_info('_iris_migrations')").all().some((c) => c.name === 'writer_version');
|
|
27
|
+
const appliedRows = db
|
|
28
|
+
.prepare(hasWriterVersion ? 'SELECT id, writer_version FROM _iris_migrations' : 'SELECT id, NULL AS writer_version FROM _iris_migrations')
|
|
29
|
+
.all();
|
|
30
|
+
/*
|
|
31
|
+
* A downgrade guard (0.9.0). Before it, a binary that did not know a
|
|
32
|
+
* migration silently ignored it and read a schema newer than itself —
|
|
33
|
+
* half the columns, none of the meaning. Now an applied id this build has
|
|
34
|
+
* never heard of refuses to start, naming the version that wrote it, so
|
|
35
|
+
* the operator upgrades instead of corrupting.
|
|
36
|
+
*/
|
|
37
|
+
const unknown = appliedRows.filter((r) => !known.has(r.id));
|
|
38
|
+
if (unknown.length > 0) {
|
|
39
|
+
const writers = [...new Set(unknown.map((r) => r.writer_version ?? 'an unknown version'))].join(', ');
|
|
40
|
+
throw new Error(`This database was migrated by a newer Iris (${writers}) — migration(s) ${unknown.map((r) => r.id).join(', ')} are unknown to v${PKG_VERSION}. Upgrade Iris, or point IRIS_DB_PATH at a database this version wrote.`);
|
|
41
|
+
}
|
|
42
|
+
const applied = new Set(appliedRows.map((r) => r.id));
|
|
26
43
|
for (const migration of migrations) {
|
|
27
44
|
if (!applied.has(migration.id)) {
|
|
28
45
|
db.transaction(() => {
|
|
@@ -31,4 +48,7 @@ export function runMigrations(db) {
|
|
|
31
48
|
})();
|
|
32
49
|
}
|
|
33
50
|
}
|
|
51
|
+
// Every applied migration names the binary that applied it (this one, for
|
|
52
|
+
// rows written before the column existed — the closest true statement).
|
|
53
|
+
db.prepare('UPDATE _iris_migrations SET writer_version = ? WHERE writer_version IS NULL').run(PKG_VERSION);
|
|
34
54
|
}
|
|
@@ -2,10 +2,19 @@ import type { IStorageAdapter, DashboardSummary, TraceQueryOptions, TraceQueryRe
|
|
|
2
2
|
import type { Trace, Span } from '../types/trace.js';
|
|
3
3
|
import type { EvalResult } from '../types/eval.js';
|
|
4
4
|
import type { TenantId } from '../types/tenant.js';
|
|
5
|
+
export type RedactMode = 'none' | 'critical_spans';
|
|
6
|
+
export interface SqliteAdapterOptions {
|
|
7
|
+
/** storage.redact — replace the spans a critical detector flagged in the stored text. */
|
|
8
|
+
redact?: RedactMode;
|
|
9
|
+
}
|
|
10
|
+
/** What every text field of an erased evaluation reads afterwards. */
|
|
11
|
+
export declare const ERASED_MESSAGE = "erased with the trace";
|
|
5
12
|
export declare class SqliteAdapter implements IStorageAdapter {
|
|
6
13
|
private db;
|
|
7
14
|
private readonly dbPath;
|
|
8
|
-
|
|
15
|
+
/** storage.redact — see SqliteAdapterOptions. */
|
|
16
|
+
private readonly redact;
|
|
17
|
+
constructor(dbPath: string, options?: SqliteAdapterOptions);
|
|
9
18
|
initialize(): Promise<void>;
|
|
10
19
|
close(): Promise<void>;
|
|
11
20
|
insertTrace(tenantId: TenantId, trace: Trace): Promise<void>;
|
|
@@ -15,6 +24,7 @@ export declare class SqliteAdapter implements IStorageAdapter {
|
|
|
15
24
|
getSpansByTraceId(tenantId: TenantId, traceId: string): Promise<Span[]>;
|
|
16
25
|
insertEvalResult(tenantId: TenantId, result: EvalResult): Promise<void>;
|
|
17
26
|
getEvalsByTraceId(tenantId: TenantId, traceId: string): Promise<EvalResult[]>;
|
|
27
|
+
getEvalById(tenantId: TenantId, id: string): Promise<EvalResult | null>;
|
|
18
28
|
queryEvalResults(tenantId: TenantId, options: {
|
|
19
29
|
eval_type?: string;
|
|
20
30
|
passed?: boolean;
|
|
@@ -41,6 +51,21 @@ export declare class SqliteAdapter implements IStorageAdapter {
|
|
|
41
51
|
}>;
|
|
42
52
|
checkpoint(): Promise<void>;
|
|
43
53
|
deleteTrace(tenantId: TenantId, traceId: string): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Blank every text field of the evaluations linked to these traces and
|
|
56
|
+
* stamp erased_at (migration 007). Verdict, scores, criticality and the
|
|
57
|
+
* evidence OFFSETS stay — they carry no text — so history and drift
|
|
58
|
+
* analytics keep working over an erased row.
|
|
59
|
+
*/
|
|
60
|
+
private eraseEvaluationsOfTraces;
|
|
61
|
+
/**
|
|
62
|
+
* storage.redact = 'critical_spans': the spans a critical detector fired
|
|
63
|
+
* on are replaced in the STORED text by [REDACTED:<pattern>]. The
|
|
64
|
+
* evidence offsets are left as computed — they index the text the caller
|
|
65
|
+
* saw, which is what a reader of the evidence needs — and the option's
|
|
66
|
+
* documentation says so.
|
|
67
|
+
*/
|
|
68
|
+
private storedOutputText;
|
|
44
69
|
getDistinctValues(tenantId: TenantId, column: string): Promise<string[]>;
|
|
45
70
|
private rowToTrace;
|
|
46
71
|
private rowToSpan;
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import Database from 'better-sqlite3';
|
|
23
23
|
import { ensureOwnerOnly } from '../utils/write-atomic.js';
|
|
24
|
+
import { deriveCoverage, deriveCriticalSkipped, deriveVerdict } from '../eval/verdict.js';
|
|
25
|
+
import { compose, DEFAULT_COMPOSE } from '../eval/compose.js';
|
|
24
26
|
import { TenantContextRequiredError } from '../types/tenant.js';
|
|
25
27
|
import { runMigrations } from './migrations/index.js';
|
|
26
28
|
const ALLOWED_SORT_COLUMNS = new Set(['timestamp', 'latency_ms', 'cost_usd']);
|
|
@@ -34,11 +36,16 @@ function assertTenant(tenantId) {
|
|
|
34
36
|
throw new TenantContextRequiredError('SqliteAdapter method invoked without a valid TenantId; refusing to query');
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
/** What every text field of an erased evaluation reads afterwards. */
|
|
40
|
+
export const ERASED_MESSAGE = 'erased with the trace';
|
|
37
41
|
export class SqliteAdapter {
|
|
38
42
|
db;
|
|
39
43
|
dbPath;
|
|
40
|
-
|
|
44
|
+
/** storage.redact — see SqliteAdapterOptions. */
|
|
45
|
+
redact;
|
|
46
|
+
constructor(dbPath, options) {
|
|
41
47
|
this.dbPath = dbPath;
|
|
48
|
+
this.redact = options?.redact ?? 'none';
|
|
42
49
|
this.db = new Database(dbPath);
|
|
43
50
|
}
|
|
44
51
|
async initialize() {
|
|
@@ -55,7 +62,14 @@ export class SqliteAdapter {
|
|
|
55
62
|
* alternative is the whole point of #372.
|
|
56
63
|
*/
|
|
57
64
|
this.db.pragma('secure_delete = ON');
|
|
58
|
-
|
|
65
|
+
try {
|
|
66
|
+
runMigrations(this.db);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// A refused boot (a newer writer, a failed migration) must not leak the handle.
|
|
70
|
+
this.db.close();
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
59
73
|
/*
|
|
60
74
|
* iris.db holds agent inputs and outputs verbatim, and a tool that
|
|
61
75
|
* detects PII necessarily stores the PII it found. better-sqlite3
|
|
@@ -203,10 +217,16 @@ export class SqliteAdapter {
|
|
|
203
217
|
* no surface could filter, count, or explain the release's flagship
|
|
204
218
|
* behaviour. NULL when nothing vetoed.
|
|
205
219
|
*/
|
|
220
|
+
/*
|
|
221
|
+
* Provenance (migration 007) is the part of the receipt a row cannot
|
|
222
|
+
* reconstruct: the release, the ruleset and config hashes, the threshold.
|
|
223
|
+
* verdict / coverage / critical_skipped are derived on every read from
|
|
224
|
+
* rule_results plus that threshold, so they are not columns.
|
|
225
|
+
*/
|
|
206
226
|
this.db.prepare(`
|
|
207
|
-
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)
|
|
208
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
209
|
-
`).run(tenantId, result.id, result.trace_id ?? null, result.eval_type, result
|
|
227
|
+
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, provenance, engine_version, ruleset_hash, config_hash, threshold, eval_cost_usd, eval_tokens)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
229
|
+
`).run(tenantId, result.id, result.trace_id ?? null, result.eval_type, this.storedOutputText(result), 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(), result.provenance ? JSON.stringify(result.provenance) : null, result.provenance?.irisVersion ?? null, result.provenance?.rulesetHash ?? null, result.provenance?.configHash ?? null, result.provenance?.thresholds.default ?? null, result.eval_cost_usd ?? null, result.eval_tokens ?? null);
|
|
210
230
|
}
|
|
211
231
|
async getEvalsByTraceId(tenantId, traceId) {
|
|
212
232
|
assertTenant(tenantId);
|
|
@@ -215,6 +235,13 @@ export class SqliteAdapter {
|
|
|
215
235
|
.all(tenantId, traceId);
|
|
216
236
|
return rows.map((row) => this.rowToEvalResult(row));
|
|
217
237
|
}
|
|
238
|
+
async getEvalById(tenantId, id) {
|
|
239
|
+
assertTenant(tenantId);
|
|
240
|
+
const row = this.db
|
|
241
|
+
.prepare('SELECT * FROM eval_results WHERE tenant_id = ? AND id = ?')
|
|
242
|
+
.get(tenantId, id);
|
|
243
|
+
return row ? this.rowToEvalResult(row) : null;
|
|
244
|
+
}
|
|
218
245
|
async queryEvalResults(tenantId, options) {
|
|
219
246
|
assertTenant(tenantId);
|
|
220
247
|
const conditions = ['tenant_id = ?'];
|
|
@@ -504,10 +531,14 @@ export class SqliteAdapter {
|
|
|
504
531
|
async deleteTracesOlderThan(tenantId, days) {
|
|
505
532
|
assertTenant(tenantId);
|
|
506
533
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
534
|
+
// Same erasure as deleteTrace: an evaluation younger than the window
|
|
535
|
+
// whose trace is swept keeps its verdict and loses its text.
|
|
536
|
+
const run = this.db.transaction((tid, cut) => {
|
|
537
|
+
const ids = this.db.prepare('SELECT trace_id FROM traces WHERE tenant_id = ? AND timestamp < ?').all(tid, cut).map((r) => r.trace_id);
|
|
538
|
+
this.eraseEvaluationsOfTraces(tid, ids);
|
|
539
|
+
return this.db.prepare('DELETE FROM traces WHERE tenant_id = ? AND timestamp < ?').run(tid, cut).changes;
|
|
540
|
+
});
|
|
541
|
+
return run(tenantId, cutoff);
|
|
511
542
|
}
|
|
512
543
|
async deleteEvalResultsOlderThan(tenantId, days) {
|
|
513
544
|
assertTenant(tenantId);
|
|
@@ -563,10 +594,80 @@ export class SqliteAdapter {
|
|
|
563
594
|
// Tenant-scoped: a trace id owned by a different tenant is
|
|
564
595
|
// untouchable from this call. Cross-tenant deletions are not just
|
|
565
596
|
// denied — they're invisible (no indication the id even exists).
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
597
|
+
//
|
|
598
|
+
// The right-to-erasure fix: eval_results.trace_id is ON DELETE SET
|
|
599
|
+
// NULL, so the delete alone left every linked evaluation behind with
|
|
600
|
+
// output_text verbatim — including what no_pii had flagged — orphaned
|
|
601
|
+
// and readable. The text is erased in the same transaction, BEFORE the
|
|
602
|
+
// FK can orphan the rows.
|
|
603
|
+
const run = this.db.transaction((tid, id) => {
|
|
604
|
+
const exists = this.db.prepare('SELECT 1 FROM traces WHERE tenant_id = ? AND trace_id = ?').get(tid, id);
|
|
605
|
+
if (!exists)
|
|
606
|
+
return 0;
|
|
607
|
+
this.eraseEvaluationsOfTraces(tid, [id]);
|
|
608
|
+
return this.db.prepare('DELETE FROM traces WHERE tenant_id = ? AND trace_id = ?').run(tid, id).changes;
|
|
609
|
+
});
|
|
610
|
+
return run(tenantId, traceId) > 0;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Blank every text field of the evaluations linked to these traces and
|
|
614
|
+
* stamp erased_at (migration 007). Verdict, scores, criticality and the
|
|
615
|
+
* evidence OFFSETS stay — they carry no text — so history and drift
|
|
616
|
+
* analytics keep working over an erased row.
|
|
617
|
+
*/
|
|
618
|
+
eraseEvaluationsOfTraces(tenantId, traceIds) {
|
|
619
|
+
if (traceIds.length === 0)
|
|
620
|
+
return 0;
|
|
621
|
+
const now = new Date().toISOString();
|
|
622
|
+
const select = this.db.prepare('SELECT id, rule_results FROM eval_results WHERE tenant_id = ? AND trace_id = ?');
|
|
623
|
+
const update = this.db.prepare('UPDATE eval_results SET output_text = ?, expected_text = NULL, suggestions = ?, rule_results = ?, erased_at = ? WHERE tenant_id = ? AND id = ?');
|
|
624
|
+
let erased = 0;
|
|
625
|
+
for (const traceId of traceIds) {
|
|
626
|
+
for (const row of select.all(tenantId, traceId)) {
|
|
627
|
+
let rules = [];
|
|
628
|
+
try {
|
|
629
|
+
rules = JSON.parse(row.rule_results);
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
rules = [];
|
|
633
|
+
}
|
|
634
|
+
const erasedRules = rules.map((r) => ({
|
|
635
|
+
...r,
|
|
636
|
+
message: ERASED_MESSAGE,
|
|
637
|
+
...(r.skipReason ? { skipReason: ERASED_MESSAGE } : {}),
|
|
638
|
+
}));
|
|
639
|
+
update.run('', '[]', JSON.stringify(erasedRules), now, tenantId, row.id);
|
|
640
|
+
erased += 1;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return erased;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* storage.redact = 'critical_spans': the spans a critical detector fired
|
|
647
|
+
* on are replaced in the STORED text by [REDACTED:<pattern>]. The
|
|
648
|
+
* evidence offsets are left as computed — they index the text the caller
|
|
649
|
+
* saw, which is what a reader of the evidence needs — and the option's
|
|
650
|
+
* documentation says so.
|
|
651
|
+
*/
|
|
652
|
+
storedOutputText(result) {
|
|
653
|
+
const text = result.output_text;
|
|
654
|
+
if (this.redact !== 'critical_spans' || !text)
|
|
655
|
+
return text ?? null;
|
|
656
|
+
const spans = result.rule_results
|
|
657
|
+
.filter((r) => r.critical === true && !r.passed && !r.skipped)
|
|
658
|
+
.flatMap((r) => (r.evidence ?? []).filter((e) => e.type === 'span' && e.source === 'output'));
|
|
659
|
+
if (spans.length === 0)
|
|
660
|
+
return text;
|
|
661
|
+
const seen = new Set();
|
|
662
|
+
let out = text;
|
|
663
|
+
for (const s of [...spans].sort((a, b) => b.start - a.start)) {
|
|
664
|
+
const key = `${s.start}:${s.end}`;
|
|
665
|
+
if (seen.has(key) || s.start >= s.end || s.end > out.length)
|
|
666
|
+
continue;
|
|
667
|
+
seen.add(key);
|
|
668
|
+
out = `${out.slice(0, s.start)}[REDACTED:${s.label}]${out.slice(s.end)}`;
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
570
671
|
}
|
|
571
672
|
async getDistinctValues(tenantId, column) {
|
|
572
673
|
assertTenant(tenantId);
|
|
@@ -613,7 +714,7 @@ export class SqliteAdapter {
|
|
|
613
714
|
};
|
|
614
715
|
}
|
|
615
716
|
rowToEvalResult(row) {
|
|
616
|
-
|
|
717
|
+
const result = {
|
|
617
718
|
id: row.id,
|
|
618
719
|
trace_id: row.trace_id,
|
|
619
720
|
eval_type: row.eval_type,
|
|
@@ -623,7 +724,8 @@ export class SqliteAdapter {
|
|
|
623
724
|
* regroup the per-bundle breakdown from what IS stored.
|
|
624
725
|
*/
|
|
625
726
|
output_text: row.output_text,
|
|
626
|
-
expected_text: row.expected_text,
|
|
727
|
+
expected_text: row.expected_text ?? undefined,
|
|
728
|
+
...(row.erased_at ? { erased_at: row.erased_at } : {}),
|
|
627
729
|
score: row.score,
|
|
628
730
|
passed: row.passed === 1,
|
|
629
731
|
rule_results: JSON.parse(row.rule_results),
|
|
@@ -640,6 +742,38 @@ export class SqliteAdapter {
|
|
|
640
742
|
...(row.critical_failures != null
|
|
641
743
|
? { critical_failures: JSON.parse(row.critical_failures) }
|
|
642
744
|
: {}),
|
|
745
|
+
...(row.eval_cost_usd != null ? { eval_cost_usd: row.eval_cost_usd } : {}),
|
|
746
|
+
...(row.eval_tokens != null ? { eval_tokens: row.eval_tokens } : {}),
|
|
747
|
+
...(row.provenance != null ? { provenance: JSON.parse(row.provenance) } : {}),
|
|
643
748
|
};
|
|
749
|
+
/*
|
|
750
|
+
* Derived on every read, never stored (0.9.0): the critical rules that
|
|
751
|
+
* skipped (from the stamped flags — absent for rows older than those
|
|
752
|
+
* flags, never []), the coverage by question, and the verdict with its
|
|
753
|
+
* basis — the last only when the row carries the threshold it was judged
|
|
754
|
+
* against, because a basis guessed against today's threshold would be a
|
|
755
|
+
* fabrication about that day.
|
|
756
|
+
*/
|
|
757
|
+
const criticalSkipped = deriveCriticalSkipped(result.rule_results);
|
|
758
|
+
if (criticalSkipped)
|
|
759
|
+
result.critical_skipped = criticalSkipped;
|
|
760
|
+
if (result.rule_results.some((r) => r.question !== undefined))
|
|
761
|
+
result.coverage = deriveCoverage(result.rule_results);
|
|
762
|
+
/*
|
|
763
|
+
* Read back with the SAME composer that wrote it, or a stored row would
|
|
764
|
+
* report a different verdict than the one the caller was given. The
|
|
765
|
+
* config is not stored (only its hash), so this composes under the
|
|
766
|
+
* shipped defaults — which is what a default-configured server used, and
|
|
767
|
+
* what `eval.composer: "legacy"` selects for a deployment that has not
|
|
768
|
+
* moved yet. A row written before the verdict existed still reads back
|
|
769
|
+
* with none: absent, never fabricated.
|
|
770
|
+
*/
|
|
771
|
+
if (result.provenance) {
|
|
772
|
+
result.verdict =
|
|
773
|
+
DEFAULT_COMPOSE.composer === 'legacy'
|
|
774
|
+
? deriveVerdict(result, result.provenance.thresholds.default)
|
|
775
|
+
: compose(result, DEFAULT_COMPOSE);
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
644
778
|
}
|
|
645
779
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { CustomRuleStore } from '../custom-rule-store.js';
|
|
3
4
|
import type { EvalEngine } from '../eval/engine.js';
|
|
5
|
+
export declare const deleteRuleOutputSchema: z.ZodObject<{
|
|
6
|
+
deleted: z.ZodBoolean;
|
|
7
|
+
rule_id: z.ZodString;
|
|
8
|
+
toggled: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
rule: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
|
|
11
|
+
}, z.core.$loose>;
|
|
4
12
|
export declare function registerDeleteRuleTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
|
|
@@ -22,6 +22,8 @@ import { z } from 'zod';
|
|
|
22
22
|
import { createCustomRule } from '../eval/rules/custom.js';
|
|
23
23
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
24
24
|
import { strictInput } from './strict-input.js';
|
|
25
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
26
|
+
import { guarded, respond } from './respond.js';
|
|
25
27
|
const inputSchema = {
|
|
26
28
|
rule_id: z
|
|
27
29
|
.string()
|
|
@@ -32,41 +34,45 @@ const inputSchema = {
|
|
|
32
34
|
.optional()
|
|
33
35
|
.describe('When present the rule is NOT deleted: false DISABLES it (kept in the store, stops firing immediately, history and provenance preserved); true RE-ENABLES a disabled rule. Omit to delete'),
|
|
34
36
|
};
|
|
37
|
+
export const deleteRuleOutputSchema = z.looseObject({
|
|
38
|
+
deleted: z.boolean().describe('true when a rule was removed; always false on a toggle'),
|
|
39
|
+
rule_id: z.string().describe('the id that was asked for'),
|
|
40
|
+
toggled: z.boolean().optional().describe('toggle only: true when the rule exists (also when it was already in the requested state)'),
|
|
41
|
+
enabled: z.boolean().optional().describe('toggle only: the rule\'s state after the call'),
|
|
42
|
+
rule: z.looseObject({}).optional().describe('toggle only: the rule as stored'),
|
|
43
|
+
});
|
|
35
44
|
export function registerDeleteRuleTool(server, customRuleStore, evalEngine) {
|
|
36
45
|
server.registerTool('delete_rule', {
|
|
37
46
|
title: 'Delete or Disable Custom Rule',
|
|
38
|
-
description:
|
|
39
|
-
'Remove a deployed custom
|
|
40
|
-
'',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"Error modes. Throws 400 on malformed rule_id (wrong prefix) or an unknown argument. Returns `{deleted: false}` (or `{toggled: 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.",
|
|
54
|
-
].join('\n'),
|
|
47
|
+
description: describeTool({
|
|
48
|
+
summary: 'Remove a deployed custom rule — or, with enabled, disable or re-enable it without removing it — effective on the next evaluate_output call.',
|
|
49
|
+
does: 'Without enabled: deletes the rule from ~/.iris/custom-rules.json, appends a rule.delete audit entry and unregisters it from the running engine; deleted is false when no rule has that id (already gone, or not this tenant\'s), and no audit row is written twice. ' +
|
|
50
|
+
'With enabled: the rule stays with its history and provenance; false stops it firing at once and keeps it off across restarts, true brings it back under the same id; a rule.toggle audit entry is written unless the flag was already in that state. ' +
|
|
51
|
+
'Past evaluations that referenced the rule are untouched either way.',
|
|
52
|
+
whenNot: 'On built-in rules: they are not in the store and cannot be deleted or disabled. To delete a trace (delete_trace). To replace a rule: deploy_rule with the same name and replace: true.',
|
|
53
|
+
returns: deleteRuleOutputSchema,
|
|
54
|
+
errors: 'IRIS_STORAGE_ERROR when the store cannot be written. A malformed rule_id (not rule-<hex>) or an unknown argument is refused before the handler runs. ' +
|
|
55
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
56
|
+
siblings: {
|
|
57
|
+
deploy_rule: 'add or replace a rule',
|
|
58
|
+
list_rules: 'find the id',
|
|
59
|
+
evaluate_output: 'where the rule fires',
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
55
62
|
inputSchema: strictInput(inputSchema),
|
|
63
|
+
outputSchema: deleteRuleOutputSchema,
|
|
56
64
|
annotations: {
|
|
57
65
|
readOnlyHint: false,
|
|
58
66
|
destructiveHint: true,
|
|
59
67
|
idempotentHint: false,
|
|
60
68
|
openWorldHint: false,
|
|
61
69
|
},
|
|
62
|
-
}, async (args) => {
|
|
70
|
+
}, guarded(async (args) => {
|
|
63
71
|
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
64
72
|
if (args.enabled !== undefined) {
|
|
65
73
|
const rule = customRuleStore.setEnabled(LOCAL_TENANT, args.rule_id, args.enabled, 'mcp');
|
|
66
74
|
if (!rule) {
|
|
67
|
-
return {
|
|
68
|
-
content: [{ type: 'text', text: JSON.stringify({ deleted: false, toggled: false, rule_id: args.rule_id }) }],
|
|
69
|
-
};
|
|
75
|
+
return respond(deleteRuleOutputSchema, { deleted: false, toggled: false, rule_id: args.rule_id });
|
|
70
76
|
}
|
|
71
77
|
// Mirror the store in the live engine, so the toggle is immediate
|
|
72
78
|
// (registerRule is idempotent by id — re-enabling an already-live
|
|
@@ -77,14 +83,7 @@ export function registerDeleteRuleTool(server, customRuleStore, evalEngine) {
|
|
|
77
83
|
else {
|
|
78
84
|
evalEngine.unregisterRule(rule.id);
|
|
79
85
|
}
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: JSON.stringify({ deleted: false, toggled: true, rule_id: args.rule_id, enabled: rule.enabled, rule }),
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
};
|
|
86
|
+
return respond(deleteRuleOutputSchema, { deleted: false, toggled: true, rule_id: args.rule_id, enabled: rule.enabled, rule });
|
|
88
87
|
}
|
|
89
88
|
const deleted = customRuleStore.delete(LOCAL_TENANT, args.rule_id, 'mcp');
|
|
90
89
|
if (deleted) {
|
|
@@ -94,13 +93,6 @@ export function registerDeleteRuleTool(server, customRuleStore, evalEngine) {
|
|
|
94
93
|
// rule was never registered in this process.
|
|
95
94
|
evalEngine.unregisterRule(args.rule_id);
|
|
96
95
|
}
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
type: 'text',
|
|
101
|
-
text: JSON.stringify({ deleted, rule_id: args.rule_id }),
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
});
|
|
96
|
+
return respond(deleteRuleOutputSchema, { deleted, rule_id: args.rule_id });
|
|
97
|
+
}));
|
|
106
98
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { IStorageAdapter } from '../types/query.js';
|
|
4
|
+
export declare const deleteTraceOutputSchema: z.ZodObject<{
|
|
5
|
+
deleted: z.ZodBoolean;
|
|
6
|
+
trace_id: z.ZodString;
|
|
7
|
+
}, z.core.$loose>;
|
|
3
8
|
export declare function registerDeleteTraceTool(server: McpServer, storage: IStorageAdapter): void;
|
|
@@ -12,48 +12,45 @@
|
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
14
14
|
import { strictInput } from './strict-input.js';
|
|
15
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
16
|
+
import { guarded, respond } from './respond.js';
|
|
15
17
|
const inputSchema = {
|
|
16
18
|
trace_id: z
|
|
17
19
|
.string()
|
|
18
20
|
.regex(/^[a-f0-9]{32}$/)
|
|
19
21
|
.describe('Trace id to delete (32-hex lowercase; obtained from log_trace response or get_traces)'),
|
|
20
22
|
};
|
|
23
|
+
export const deleteTraceOutputSchema = z.looseObject({
|
|
24
|
+
deleted: z.boolean().describe('true when a trace row was removed; false when no trace with that id existed for this tenant'),
|
|
25
|
+
trace_id: z.string().describe('the id that was asked for'),
|
|
26
|
+
});
|
|
21
27
|
export function registerDeleteTraceTool(server, storage) {
|
|
22
28
|
server.registerTool('delete_trace', {
|
|
23
29
|
title: 'Delete Trace',
|
|
24
|
-
description:
|
|
25
|
-
'Remove
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
'',
|
|
29
|
-
|
|
30
|
-
''
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.",
|
|
40
|
-
].join('\n'),
|
|
30
|
+
description: describeTool({
|
|
31
|
+
summary: 'Remove one stored trace by id; its spans go with it, and every evaluation linked to it keeps its verdict and loses its text.',
|
|
32
|
+
does: "Deletes the trace row for the caller's tenant. Spans cascade. Evaluations linked to it keep their verdict, scores, criticality and evidence offsets; their output text, expected text, suggestions and rule messages are erased in the same transaction and erased_at is stamped, so no text from the trace survives in any evaluation. " +
|
|
33
|
+
"deleted is false when no trace has that id — already removed, or not this tenant's — and that is not an error. No audit entry is written: traces are user data, not policy.",
|
|
34
|
+
whenNot: 'To expire old data in bulk (retention.days; the sweep runs at boot and every retention.sweepIntervalHours). To delete evaluations: they are not deleted per row; retention and --purge cover them. To pause anything: traces are immutable, there is nothing to pause.',
|
|
35
|
+
returns: deleteTraceOutputSchema,
|
|
36
|
+
errors: 'IRIS_STORAGE_ERROR when the delete cannot run. A malformed trace_id (not 32 lowercase hex) is refused before the handler runs. ' +
|
|
37
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
38
|
+
siblings: {
|
|
39
|
+
log_trace: 'store a trace',
|
|
40
|
+
get_traces: 'find the trace to delete',
|
|
41
|
+
delete_rule: 'the equivalent for custom rules',
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
41
44
|
inputSchema: strictInput(inputSchema),
|
|
45
|
+
outputSchema: deleteTraceOutputSchema,
|
|
42
46
|
annotations: {
|
|
43
47
|
readOnlyHint: false,
|
|
44
48
|
destructiveHint: true,
|
|
45
49
|
idempotentHint: false,
|
|
46
50
|
openWorldHint: false,
|
|
47
51
|
},
|
|
48
|
-
}, async (args) => {
|
|
52
|
+
}, guarded(async (args) => {
|
|
49
53
|
const deleted = await storage.deleteTrace(LOCAL_TENANT, args.trace_id);
|
|
50
|
-
return {
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
type: 'text',
|
|
54
|
-
text: JSON.stringify({ deleted, trace_id: args.trace_id }),
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
});
|
|
54
|
+
return respond(deleteTraceOutputSchema, { deleted, trace_id: args.trace_id });
|
|
55
|
+
}));
|
|
59
56
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { CustomRuleStore } from '../custom-rule-store.js';
|
|
3
4
|
import type { EvalEngine } from '../eval/engine.js';
|
|
@@ -6,7 +7,8 @@ import { type TenantId } from '../types/tenant.js';
|
|
|
6
7
|
/**
|
|
7
8
|
* A rule with this name is already deployed and the caller did not ask to
|
|
8
9
|
* replace it. Carries the existing rule(s) so an HTTP surface can answer
|
|
9
|
-
* 409 with them beside the same message the MCP tool
|
|
10
|
+
* 409 with them beside the same message the MCP tool returns as
|
|
11
|
+
* IRIS_DUPLICATE_RULE (src/tools/errors.ts maps it by name).
|
|
10
12
|
*/
|
|
11
13
|
export declare class DuplicateRuleNameError extends Error {
|
|
12
14
|
readonly existing: DeployedCustomRule[];
|
|
@@ -34,4 +36,14 @@ export interface ReplacedRule {
|
|
|
34
36
|
export declare function retireSameNamedRules(store: CustomRuleStore, engine: EvalEngine, tenantId: TenantId, name: string, replace: boolean, user: string): ReplacedRule[];
|
|
35
37
|
/** The `warning` both deploy surfaces attach when a replace retired rules. */
|
|
36
38
|
export declare function replacedRulesWarning(name: string, replaced: ReplacedRule[]): string;
|
|
39
|
+
export declare const deployRuleOutputSchema: z.ZodObject<{
|
|
40
|
+
rule: z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
}, z.core.$loose>;
|
|
44
|
+
replaced: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
}, z.core.$loose>>>;
|
|
47
|
+
warning: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, z.core.$loose>;
|
|
37
49
|
export declare function registerDeployRuleTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
|