@iris-eval/mcp-server 0.8.2 → 0.9.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 +8 -1
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +2 -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-Cz8_oOqG.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 +9 -28
- 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/criticality.d.ts +8 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +1 -0
- package/dist/eval/engine.js +49 -3
- 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 +10 -0
- package/dist/eval/llm-judge/evaluator.js +16 -1
- 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/rules/completeness.js +31 -0
- package/dist/eval/rules/cost.d.ts +1 -1
- package/dist/eval/rules/cost.js +44 -0
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +21 -0
- package/dist/eval/rules/relevance.js +16 -0
- package/dist/eval/rules/safety.js +178 -1
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +88 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -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 +14 -0
- 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 +135 -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 +37 -58
- package/dist/tools/evaluate-with-llm-judge.d.ts +31 -0
- package/dist/tools/evaluate-with-llm-judge.js +96 -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 +17 -2
- package/dist/tools/verify-citations.js +98 -93
- package/dist/types/config.d.ts +9 -0
- package/dist/types/eval.d.ts +258 -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
|
@@ -1,41 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
3
|
import { verifyCitations } from '../eval/citation-verify/verifier.js';
|
|
4
|
-
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
4
|
import { generateEvalId } from '../utils/ids.js';
|
|
5
|
+
import { JUDGE_KEY_VARS } from '../judge-enablement.js';
|
|
6
6
|
import { strictInput } from './strict-input.js';
|
|
7
7
|
import { assertTraceExists, insertLinkedEvalResult } from './trace-link.js';
|
|
8
|
+
import { inferProvider, resolveApiKey } from './evaluate-with-llm-judge.js';
|
|
9
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
10
|
+
import { irisError } from './errors.js';
|
|
11
|
+
import { evaluationLinks, guarded, respond } from './respond.js';
|
|
8
12
|
const inputSchema = {
|
|
9
13
|
output: z.string().min(1).describe('The agent output containing citations to verify'),
|
|
10
14
|
model: z
|
|
11
15
|
.string()
|
|
12
|
-
.describe('Judge model for per-citation verification. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini.'),
|
|
16
|
+
.describe('Judge model for per-citation verification. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini.'),
|
|
13
17
|
provider: z.enum(['anthropic', 'openai']).optional().describe('Auto-detected from model when omitted'),
|
|
14
18
|
allow_fetch: z.boolean().optional().describe('Permit outbound HTTP to resolve URLs/DOIs. Defaults to IRIS_CITATION_ALLOW_FETCH=1; false otherwise. SSRF-guarded regardless.'),
|
|
15
19
|
domain_allowlist: z
|
|
16
20
|
.array(z.string())
|
|
17
21
|
.optional()
|
|
18
22
|
.describe('Restrict fetches to hostnames in this list (suffix match allowed). Merged with IRIS_CITATION_DOMAINS env.'),
|
|
19
|
-
max_cost_usd_total: z.number().positive().optional().describe('Cap TOTAL judge cost across all citations in this call; default
|
|
20
|
-
max_citations: z.number().int().positive().max(50).optional().describe('Max citations to verify (extras skipped); default 20'),
|
|
23
|
+
max_cost_usd_total: z.number().positive().optional().describe('Cap TOTAL judge cost across all citations in this call; default 1.00 USD — the pipeline stops when the next call would exceed it'),
|
|
24
|
+
max_citations: z.number().int().positive().max(50).optional().describe('Max citations to verify (extras skipped, not errored); default 20, at most 50'),
|
|
21
25
|
per_source_timeout_ms: z.number().int().positive().optional().describe('Per-URL fetch timeout; default 10_000'),
|
|
22
26
|
per_source_max_bytes: z.number().int().positive().optional().describe('Per-URL body cap; default 5MB'),
|
|
23
27
|
trace_id: z.string().optional().describe('Link verification result to a stored trace (id from log_trace / get_traces); an unknown id is rejected before any fetch or judge call'),
|
|
24
28
|
};
|
|
25
|
-
function inferProvider(model) {
|
|
26
|
-
const pricing = findPricing(model);
|
|
27
|
-
if (!pricing) {
|
|
28
|
-
throw new Error(`Unknown model "${model}". Provider cannot be inferred. Supported models: src/eval/llm-judge/pricing.ts.`);
|
|
29
|
-
}
|
|
30
|
-
return pricing.provider;
|
|
31
|
-
}
|
|
32
|
-
function resolveApiKey(provider) {
|
|
33
|
-
const key = provider === 'anthropic' ? process.env.IRIS_ANTHROPIC_API_KEY : process.env.IRIS_OPENAI_API_KEY;
|
|
34
|
-
if (!key) {
|
|
35
|
-
throw new Error(`${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} judge requires IRIS_${provider === 'anthropic' ? 'ANTHROPIC' : 'OPENAI'}_API_KEY for verify_citations.`);
|
|
36
|
-
}
|
|
37
|
-
return key;
|
|
38
|
-
}
|
|
39
29
|
function resolveAllowFetch(paramValue) {
|
|
40
30
|
if (paramValue !== undefined)
|
|
41
31
|
return paramValue;
|
|
@@ -54,8 +44,8 @@ function resolveDomainAllowlist(paramValue) {
|
|
|
54
44
|
* was nothing to judge (no citations, none resolved). It is NOT the honest
|
|
55
45
|
* answer when citations resolved and the judge then failed on every one —
|
|
56
46
|
* a wrong API key, a model the provider refused, a parse failure — because
|
|
57
|
-
* the caller reads "passed" and ships. That case is
|
|
58
|
-
* cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
47
|
+
* the caller reads "passed" and ships. That case is IRIS_JUDGE_FAILED
|
|
48
|
+
* naming the cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
59
49
|
*/
|
|
60
50
|
export function assertJudgeRan(result) {
|
|
61
51
|
if (result.totalResolved === 0 || result.totalJudged > 0)
|
|
@@ -65,39 +55,59 @@ export function assertJudgeRan(result) {
|
|
|
65
55
|
return;
|
|
66
56
|
const kinds = [...new Set(judgeFailures.map((c) => c.resolveError.kind))].join(', ');
|
|
67
57
|
const first = judgeFailures[0].resolveError.message;
|
|
68
|
-
throw
|
|
69
|
-
`Nothing was verified and nothing was stored, so there is no verdict. First error: ${first}
|
|
58
|
+
throw irisError('IRIS_JUDGE_FAILED', `verify_citations could not judge any of the ${result.totalResolved} resolved citation(s): the judge failed on every one (${kinds}). ` +
|
|
59
|
+
`Nothing was verified and nothing was stored, so there is no verdict. First error: ${first}`, {
|
|
60
|
+
retryable: /timeout|rate_limit|server_error/.test(kinds),
|
|
61
|
+
recovery: [
|
|
62
|
+
'Check the key and the model: a refused key or an unknown model fails every citation the same way.',
|
|
63
|
+
'Retry when the kind is a timeout, a rate limit or a provider server error.',
|
|
64
|
+
'Raise max_cost_usd_total when the kind is cost_cap_reached.',
|
|
65
|
+
],
|
|
66
|
+
});
|
|
70
67
|
}
|
|
68
|
+
export const verifyCitationsOutputSchema = z.looseObject({
|
|
69
|
+
id: z.string().describe('the evaluation id; read it back at iris://evaluations/{id}'),
|
|
70
|
+
trace_id: z.string().optional().describe('the linked trace, when one was named'),
|
|
71
|
+
overall_score: z.number().nullable().describe('supported / judged; null when nothing was judged'),
|
|
72
|
+
passed: z.boolean().describe('true when every judged citation was supported, or nothing was judged and nothing failed'),
|
|
73
|
+
total_citations_found: z.number().int().describe('citations extracted from the output'),
|
|
74
|
+
total_resolved: z.number().int().describe('citations whose source was fetched'),
|
|
75
|
+
total_judged: z.number().int().describe('citations the judge ruled on'),
|
|
76
|
+
total_supported: z.number().int().describe('citations the judge found supported'),
|
|
77
|
+
total_cost_usd: z.number().describe('the spend across every judge call'),
|
|
78
|
+
citations: z.array(z.looseObject({ resolve_status: z.string() })).describe('per citation: the citation (raw, kind, identifier, offsets), resolve_status ok | skipped | error, resolve_error, source (url, status, content_type, bytes_fetched, truncated), judge (supported, confidence, rationale, cost_usd, latency_ms, tokens)'),
|
|
79
|
+
});
|
|
71
80
|
export function registerVerifyCitationsTool(server, storage) {
|
|
72
81
|
server.registerTool('verify_citations', {
|
|
73
82
|
title: 'Verify Citations',
|
|
74
|
-
description:
|
|
75
|
-
'Extract citations
|
|
76
|
-
''
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
description: describeTool({
|
|
84
|
+
summary: 'Extract the citations in an output, fetch the sources (opt-in, SSRF-guarded) and ask an LLM judge on your key whether each source supports its claim.',
|
|
85
|
+
does: 'Three phases. Extraction, no network: [N] references, (Author, Year), bare URLs and DOIs. Fetch of URL and DOI citations only when allow_fetch is true or IRIS_CITATION_ALLOW_FETCH=1, through a scheme allowlist, private and cloud-metadata address blocking, an optional hostname allowlist (domain_allowlist, merged with IRIS_CITATION_DOMAINS), a per-source timeout and byte cap, and at most three re-checked redirects. ' +
|
|
86
|
+
'Then one judge call per resolved citation on your own key, reading the first part of each source, capped in total by max_cost_usd_total. Up to max_citations are verified; extras are skipped, not errored. ' +
|
|
87
|
+
'overall_score is supported / judged and null when nothing was judged. Per-citation failures (bad scheme, blocked address, timeout, too large, cost cap, fetch disabled) are reported on the citation, never scored as unsupported. One evaluation row is stored.',
|
|
88
|
+
whenNot: "When the output has no citations: the score is null, and evaluate_output's hallucination signals are the cheap check. " +
|
|
89
|
+
`Without a key (${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai}): the call returns IRIS_JUDGE_NOT_ENABLED with the enable steps. ` +
|
|
90
|
+
'With fetch enabled and an open allowlist on untrusted output: you are running a user-directed fetcher — set IRIS_CITATION_DOMAINS.',
|
|
91
|
+
returns: verifyCitationsOutputSchema,
|
|
92
|
+
errors: 'IRIS_JUDGE_NOT_ENABLED, IRIS_JUDGE_UNKNOWN_MODEL and IRIS_UNKNOWN_TRACE before any fetch or spend. IRIS_JUDGE_FAILED when citations resolved but the judge failed on every one — an error, not a passing verdict; nothing is stored. ' +
|
|
93
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
94
|
+
siblings: {
|
|
95
|
+
evaluate_with_llm_judge: 'general semantic scoring',
|
|
96
|
+
evaluate_output: 'the free deterministic path, including the hallucination signals',
|
|
97
|
+
log_trace: 'record the execution first',
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
91
100
|
inputSchema: strictInput(inputSchema),
|
|
101
|
+
outputSchema: verifyCitationsOutputSchema,
|
|
92
102
|
annotations: {
|
|
93
103
|
readOnlyHint: false, // Writes eval_result + spends money
|
|
94
104
|
destructiveHint: false, // Creates data; doesn't overwrite/delete
|
|
95
105
|
idempotentHint: false, // External fetches + provider non-determinism
|
|
96
106
|
openWorldHint: true, // Outbound HTTP to citation URLs + LLM provider API
|
|
97
107
|
},
|
|
98
|
-
}, async (args) => {
|
|
108
|
+
}, guarded(async (args) => {
|
|
99
109
|
const provider = args.provider ?? inferProvider(args.model);
|
|
100
|
-
const apiKey = resolveApiKey(provider);
|
|
110
|
+
const apiKey = resolveApiKey(provider, 'verify_citations');
|
|
101
111
|
const allowFetch = resolveAllowFetch(args.allow_fetch);
|
|
102
112
|
const domainAllowlist = resolveDomainAllowlist(args.domain_allowlist);
|
|
103
113
|
// Refused before any fetch or judge call spends anything (#376).
|
|
@@ -143,58 +153,53 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
143
153
|
rules_evaluated: 1,
|
|
144
154
|
rules_skipped: 0,
|
|
145
155
|
insufficient_data: result.overallScore === null,
|
|
156
|
+
eval_cost_usd: result.totalCostUsd,
|
|
146
157
|
});
|
|
147
|
-
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
identifier: c.citation.identifier,
|
|
165
|
-
offset_start: c.citation.offsetStart,
|
|
166
|
-
offset_end: c.citation.offsetEnd,
|
|
167
|
-
},
|
|
168
|
-
resolve_status: c.resolveStatus,
|
|
169
|
-
resolve_error: c.resolveError,
|
|
170
|
-
// Mapped to the documented snake_case keys. The verifier's
|
|
171
|
-
// internal shape is camelCase (contentType, bytesFetched) and
|
|
172
|
-
// used to be passed through verbatim, so a client parsing
|
|
173
|
-
// `source.content_type` per the description read undefined.
|
|
174
|
-
source: c.source
|
|
175
|
-
? {
|
|
176
|
-
url: c.source.url,
|
|
177
|
-
status: c.source.status,
|
|
178
|
-
content_type: c.source.contentType,
|
|
179
|
-
bytes_fetched: c.source.bytesFetched,
|
|
180
|
-
truncated: c.source.truncated,
|
|
181
|
-
}
|
|
182
|
-
: undefined,
|
|
183
|
-
judge: c.judge
|
|
184
|
-
? {
|
|
185
|
-
supported: c.judge.supported,
|
|
186
|
-
confidence: c.judge.confidence,
|
|
187
|
-
rationale: c.judge.rationale,
|
|
188
|
-
cost_usd: c.judge.costUsd,
|
|
189
|
-
latency_ms: c.judge.latencyMs,
|
|
190
|
-
input_tokens: c.judge.inputTokens,
|
|
191
|
-
output_tokens: c.judge.outputTokens,
|
|
192
|
-
}
|
|
193
|
-
: undefined,
|
|
194
|
-
})),
|
|
195
|
-
}),
|
|
158
|
+
return respond(verifyCitationsOutputSchema, {
|
|
159
|
+
id: evalId,
|
|
160
|
+
...(args.trace_id ? { trace_id: args.trace_id } : {}),
|
|
161
|
+
overall_score: result.overallScore,
|
|
162
|
+
passed: result.passed,
|
|
163
|
+
total_citations_found: result.totalCitationsFound,
|
|
164
|
+
total_resolved: result.totalResolved,
|
|
165
|
+
total_judged: result.totalJudged,
|
|
166
|
+
total_supported: result.totalSupported,
|
|
167
|
+
total_cost_usd: result.totalCostUsd,
|
|
168
|
+
citations: result.citations.map((c) => ({
|
|
169
|
+
citation: {
|
|
170
|
+
raw: c.citation.raw,
|
|
171
|
+
kind: c.citation.kind,
|
|
172
|
+
identifier: c.citation.identifier,
|
|
173
|
+
offset_start: c.citation.offsetStart,
|
|
174
|
+
offset_end: c.citation.offsetEnd,
|
|
196
175
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
176
|
+
resolve_status: c.resolveStatus,
|
|
177
|
+
resolve_error: c.resolveError,
|
|
178
|
+
// Mapped to the documented snake_case keys. The verifier's
|
|
179
|
+
// internal shape is camelCase (contentType, bytesFetched) and
|
|
180
|
+
// used to be passed through verbatim, so a client parsing
|
|
181
|
+
// `source.content_type` per the description read undefined.
|
|
182
|
+
source: c.source
|
|
183
|
+
? {
|
|
184
|
+
url: c.source.url,
|
|
185
|
+
status: c.source.status,
|
|
186
|
+
content_type: c.source.contentType,
|
|
187
|
+
bytes_fetched: c.source.bytesFetched,
|
|
188
|
+
truncated: c.source.truncated,
|
|
189
|
+
}
|
|
190
|
+
: undefined,
|
|
191
|
+
judge: c.judge
|
|
192
|
+
? {
|
|
193
|
+
supported: c.judge.supported,
|
|
194
|
+
confidence: c.judge.confidence,
|
|
195
|
+
rationale: c.judge.rationale,
|
|
196
|
+
cost_usd: c.judge.costUsd,
|
|
197
|
+
latency_ms: c.judge.latencyMs,
|
|
198
|
+
input_tokens: c.judge.inputTokens,
|
|
199
|
+
output_tokens: c.judge.outputTokens,
|
|
200
|
+
}
|
|
201
|
+
: undefined,
|
|
202
|
+
})),
|
|
203
|
+
}, evaluationLinks(evalId, args.trace_id));
|
|
204
|
+
}));
|
|
200
205
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ export interface IrisConfig {
|
|
|
2
2
|
storage: {
|
|
3
3
|
type: 'sqlite';
|
|
4
4
|
path: string;
|
|
5
|
+
/**
|
|
6
|
+
* `critical_spans` stores each evaluation's output text with the spans a
|
|
7
|
+
* critical detector flagged replaced by `[REDACTED:<pattern>]`, so a tool
|
|
8
|
+
* that detects leaks need not keep the leak it found. The evidence
|
|
9
|
+
* offsets still index the original text the caller saw. Default `none`.
|
|
10
|
+
*/
|
|
11
|
+
redact?: 'none' | 'critical_spans';
|
|
5
12
|
};
|
|
6
13
|
server: {
|
|
7
14
|
name: string;
|
|
@@ -53,6 +60,8 @@ export interface IrisConfig {
|
|
|
53
60
|
};
|
|
54
61
|
retention: {
|
|
55
62
|
days: number;
|
|
63
|
+
/** How often the sweep re-runs after boot, in hours; 0 disables the timer (the boot sweep still runs). Default 24. */
|
|
64
|
+
sweepIntervalHours: number;
|
|
56
65
|
};
|
|
57
66
|
security: {
|
|
58
67
|
apiKey?: string;
|
package/dist/types/eval.d.ts
CHANGED
|
@@ -8,6 +8,23 @@ export type EvalType = 'completeness' | 'relevance' | 'safety' | 'cost' | 'custo
|
|
|
8
8
|
* under a real bundle, never under 'all'.
|
|
9
9
|
*/
|
|
10
10
|
export type EvalResultType = EvalType | 'all';
|
|
11
|
+
/**
|
|
12
|
+
* What KIND of claim a rule makes — the mandate's distinction between a
|
|
13
|
+
* measurement (a statistic against a threshold), a detection (a pattern is
|
|
14
|
+
* present, with a measured error rate), an inference (a signal standing in
|
|
15
|
+
* for an unobservable property), a judgment (a model's reasoning), a policy
|
|
16
|
+
* (the deployment's own constraint) and an external verification. Kind is
|
|
17
|
+
* the claim; `mechanism` is how the claim is measured. The composer decides
|
|
18
|
+
* by kind and never averages kinds together.
|
|
19
|
+
*/
|
|
20
|
+
export type ClaimKind = 'measurement' | 'detection' | 'inference' | 'judgment' | 'policy' | 'verification';
|
|
21
|
+
export type Mechanism = 'formula' | 'pattern' | 'heuristic' | 'model' | 'external';
|
|
22
|
+
/** An input a rule reads. A rule skips — never passes — when a declared need is absent. */
|
|
23
|
+
export type Need = 'output' | 'input' | 'expected' | 'tool_calls' | 'tool_outputs' | 'tools_catalogue' | 'cost' | 'tokens' | 'citations';
|
|
24
|
+
/** The evaluation question a rule answers; the registry is src/eval/questions.ts. */
|
|
25
|
+
export type QuestionId = 'safe_output' | 'grounded' | 'complete' | 'relevant' | 'task_completed' | 'tool_use_correct' | 'within_budget';
|
|
26
|
+
/** What went wrong, in the reader's words, independent of which rule caught it; the registry is src/eval/failure-classes.ts. */
|
|
27
|
+
export type FailureClass = 'pii_leak' | 'credential_leak' | 'injection' | 'injection_compliance' | 'silent_tool_failure' | 'tool_loop' | 'stub' | 'fabrication' | 'ungrounded' | 'incomplete_ask' | 'off_task' | 'over_budget' | 'format' | 'invalid_tool_call';
|
|
11
28
|
export interface EvalRule {
|
|
12
29
|
name: string;
|
|
13
30
|
description: string;
|
|
@@ -25,6 +42,23 @@ export interface EvalRule {
|
|
|
25
42
|
* is the verdict, and a critical violation must never be averaged away.
|
|
26
43
|
*/
|
|
27
44
|
critical?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The rule's metadata — what kind of claim it makes, how it measures it,
|
|
47
|
+
* what it reads, which question it answers, which failure classes a
|
|
48
|
+
* failing result belongs to, and the version of its definition. Every
|
|
49
|
+
* built-in declares all six (tests/unit/eval/rule-metadata.test.ts);
|
|
50
|
+
* custom types declare kind, mechanism, needs and version and leave the
|
|
51
|
+
* question to their author. Optional on the interface so a rule built
|
|
52
|
+
* elsewhere still compiles; a result from a rule without them carries no
|
|
53
|
+
* `kind`, which reads as unknown — never as a measurement.
|
|
54
|
+
*/
|
|
55
|
+
kind?: ClaimKind;
|
|
56
|
+
mechanism?: Mechanism;
|
|
57
|
+
needs?: readonly Need[];
|
|
58
|
+
question?: QuestionId;
|
|
59
|
+
classes?: readonly FailureClass[];
|
|
60
|
+
/** Bumped when the rule's meaning changes, so a stored result names the definition that produced it. */
|
|
61
|
+
version?: number;
|
|
28
62
|
evaluate(context: EvalContext): EvalRuleResult;
|
|
29
63
|
}
|
|
30
64
|
export interface EvalContext {
|
|
@@ -64,8 +98,221 @@ export interface EvalContext {
|
|
|
64
98
|
breaches: number;
|
|
65
99
|
};
|
|
66
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* What the composer DID with a result under this deployment's configuration
|
|
103
|
+
* — distinct from `kind`, which is what the rule claims. Today's composer
|
|
104
|
+
* (a weighted mean plus the critical veto) knows two roles: `veto` for an
|
|
105
|
+
* effectively critical rule and `term` for one that feeds the score. The
|
|
106
|
+
* compose-by-kind release adds `gate` (a configured policy that decides),
|
|
107
|
+
* `risk` (a detection or inference feeding the risk estimate) and
|
|
108
|
+
* `advisory` (reported, deciding nothing).
|
|
109
|
+
*/
|
|
110
|
+
export type Role = 'gate' | 'veto' | 'risk' | 'advisory' | 'term';
|
|
111
|
+
/**
|
|
112
|
+
* Why a rule skipped. `not_applicable`: the evidence it needs was not
|
|
113
|
+
* supplied (never asked — coverage). `defeated`: asked and could not answer,
|
|
114
|
+
* because this output stalled its pattern past the sandbox budget.
|
|
115
|
+
* `config_invalid`: asked and could not answer, because its definition is
|
|
116
|
+
* broken. A gate that fails closed treats the last two as unknown; the first
|
|
117
|
+
* is a coverage fact, not a verdict.
|
|
118
|
+
*/
|
|
119
|
+
export type SkipClass = 'not_applicable' | 'defeated' | 'config_invalid';
|
|
120
|
+
export interface Interval {
|
|
121
|
+
point: number;
|
|
122
|
+
lo: number;
|
|
123
|
+
hi: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a rule saw — typed, locatable, never an excerpt. A detection reports
|
|
127
|
+
* the OFFSETS of what it matched (into the raw text, so a leak detector can
|
|
128
|
+
* redact the span it found without ever repeating it); a trajectory rule the
|
|
129
|
+
* index of the call it judged; a measurement its statistic with a unit and
|
|
130
|
+
* the threshold it was held to; a signal that yields no offset yet reports
|
|
131
|
+
* its name and count. The reader can locate every claim; the stored row
|
|
132
|
+
* can be redacted; nothing here restates the offending text.
|
|
133
|
+
*/
|
|
134
|
+
export type Evidence = {
|
|
135
|
+
type: 'span';
|
|
136
|
+
source: 'output' | 'input' | `tool_outputs[${number}]`;
|
|
137
|
+
start: number;
|
|
138
|
+
end: number;
|
|
139
|
+
label: string;
|
|
140
|
+
} | {
|
|
141
|
+
type: 'pattern';
|
|
142
|
+
name: string;
|
|
143
|
+
count: number;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'toolCall';
|
|
146
|
+
index: number;
|
|
147
|
+
toolName: string;
|
|
148
|
+
label: string;
|
|
149
|
+
} | {
|
|
150
|
+
type: 'citation';
|
|
151
|
+
url: string;
|
|
152
|
+
status: 'resolved' | 'dead' | 'unverifiable' | 'supported' | 'unsupported';
|
|
153
|
+
} | {
|
|
154
|
+
type: 'count';
|
|
155
|
+
stat: string;
|
|
156
|
+
unit: string;
|
|
157
|
+
value: number;
|
|
158
|
+
threshold?: number;
|
|
159
|
+
thresholdSource?: 'default' | 'config' | 'call' | 'rule';
|
|
160
|
+
};
|
|
161
|
+
/** A measurement's statistic — the number the rule computed, with its unit, before any score transform. */
|
|
162
|
+
export interface MeasuredValue {
|
|
163
|
+
stat: string;
|
|
164
|
+
unit: string;
|
|
165
|
+
value: number;
|
|
166
|
+
}
|
|
167
|
+
/** Evidence lists are capped so a pathological output cannot balloon a stored row. */
|
|
168
|
+
export declare const MAX_EVIDENCE_ITEMS = 25;
|
|
169
|
+
/**
|
|
170
|
+
* Which evaluation questions this evaluation judged, which it did not and
|
|
171
|
+
* why — coverage by question, not by rule count. `inputs` says what the
|
|
172
|
+
* call carried; a question is `judged` when at least one rule that answers
|
|
173
|
+
* it ran, `unjudged` when every such rule skipped (the reason names the
|
|
174
|
+
* missing input, or that the rule was defeated or broken), and
|
|
175
|
+
* `not_applicable` when no rule for it was in the selected bundles.
|
|
176
|
+
*/
|
|
177
|
+
export interface Coverage {
|
|
178
|
+
inputs: Record<Need, boolean>;
|
|
179
|
+
questions: Array<{
|
|
180
|
+
id: QuestionId;
|
|
181
|
+
status: 'judged' | 'unjudged' | 'not_applicable';
|
|
182
|
+
why?: string;
|
|
183
|
+
}>;
|
|
184
|
+
/** Quarantined critical rules that did not run (surfaced by the rule-store release). */
|
|
185
|
+
dormant?: Array<{
|
|
186
|
+
ruleId: string;
|
|
187
|
+
name: string;
|
|
188
|
+
reason: string;
|
|
189
|
+
}>;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The verdict with its basis. `passed` is `state === 'pass'` and equals the
|
|
193
|
+
* top-level `passed`; `basis` says which layer decided — a configured policy,
|
|
194
|
+
* a detector's veto, nothing judged, or the score against the threshold.
|
|
195
|
+
* `risk` is null until the compose-by-kind release computes it.
|
|
196
|
+
*/
|
|
197
|
+
export interface Verdict {
|
|
198
|
+
state: 'pass' | 'fail' | 'unknown';
|
|
199
|
+
passed: boolean;
|
|
200
|
+
basis: 'policy_gate' | 'detector_veto' | 'critical_unknown' | 'required_evidence_missing' | 'risk_over_loss' | 'score_below_threshold' | 'clean' | 'no_rules';
|
|
201
|
+
by: string[];
|
|
202
|
+
risk: {
|
|
203
|
+
pBad: number;
|
|
204
|
+
lo: number;
|
|
205
|
+
hi: number;
|
|
206
|
+
perClass: Partial<Record<FailureClass, number | null>>;
|
|
207
|
+
assumptions: string[];
|
|
208
|
+
} | null;
|
|
209
|
+
confidence?: 'decisive' | 'marginal';
|
|
210
|
+
}
|
|
211
|
+
/** What produced this verdict, so it can be replayed or compared: the release, the ruleset, the configuration, the thresholds, the proof corpus, the time. */
|
|
212
|
+
export interface Provenance {
|
|
213
|
+
irisVersion: string;
|
|
214
|
+
rulesetHash: string;
|
|
215
|
+
configHash: string;
|
|
216
|
+
thresholds: {
|
|
217
|
+
default: number;
|
|
218
|
+
perRule?: Record<string, unknown>;
|
|
219
|
+
};
|
|
220
|
+
corpusVersion: string;
|
|
221
|
+
judgedAt: string;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* How wrong this result tends to be, and on what basis. `published_accuracy`
|
|
225
|
+
* carries the rule's measured numbers from the shipped proof (src/eval/
|
|
226
|
+
* published-accuracy.ts): for a fired detection or inference the positive
|
|
227
|
+
* predictive value at the stated prior, for one that did not fire the
|
|
228
|
+
* residual miss rate, each with a 95% credible interval. `definition` is a
|
|
229
|
+
* measurement's conformance to its formula (n cases, matched). `policy` is
|
|
230
|
+
* the deployment's own constraint — no error rate applies. `self_consistency`
|
|
231
|
+
* and `local_labels` arrive with the judge-through-the-engine and the
|
|
232
|
+
* own-traffic labels releases. `unmeasured` says why nothing can be stated.
|
|
233
|
+
*/
|
|
234
|
+
export type Uncertainty = {
|
|
235
|
+
basis: 'published_accuracy';
|
|
236
|
+
fired: true;
|
|
237
|
+
ppv: Interval;
|
|
238
|
+
prior: {
|
|
239
|
+
pi: number;
|
|
240
|
+
source: 'default' | 'config' | 'estimated';
|
|
241
|
+
};
|
|
242
|
+
corpus: {
|
|
243
|
+
n: number;
|
|
244
|
+
tp: number;
|
|
245
|
+
fp: number;
|
|
246
|
+
fn: number;
|
|
247
|
+
tn: number;
|
|
248
|
+
version: string;
|
|
249
|
+
release: string;
|
|
250
|
+
labelling: 'same-model' | 'human-verified';
|
|
251
|
+
};
|
|
252
|
+
} | {
|
|
253
|
+
basis: 'published_accuracy';
|
|
254
|
+
fired: false;
|
|
255
|
+
missRate: Interval;
|
|
256
|
+
prior: {
|
|
257
|
+
pi: number;
|
|
258
|
+
source: 'default' | 'config' | 'estimated';
|
|
259
|
+
};
|
|
260
|
+
corpus: {
|
|
261
|
+
n: number;
|
|
262
|
+
tp: number;
|
|
263
|
+
fp: number;
|
|
264
|
+
fn: number;
|
|
265
|
+
tn: number;
|
|
266
|
+
version: string;
|
|
267
|
+
release: string;
|
|
268
|
+
labelling: 'same-model' | 'human-verified';
|
|
269
|
+
};
|
|
270
|
+
} | {
|
|
271
|
+
basis: 'definition';
|
|
272
|
+
conformance: {
|
|
273
|
+
n: number;
|
|
274
|
+
matched: number;
|
|
275
|
+
};
|
|
276
|
+
} | {
|
|
277
|
+
basis: 'self_consistency';
|
|
278
|
+
samples: number;
|
|
279
|
+
voteFraction: number;
|
|
280
|
+
scoreSd: number;
|
|
281
|
+
} | {
|
|
282
|
+
basis: 'local_labels';
|
|
283
|
+
precision: Interval;
|
|
284
|
+
n: number;
|
|
285
|
+
} | {
|
|
286
|
+
basis: 'policy';
|
|
287
|
+
} | {
|
|
288
|
+
basis: 'unmeasured';
|
|
289
|
+
why: string;
|
|
290
|
+
};
|
|
67
291
|
export interface EvalRuleResult {
|
|
68
292
|
ruleName: string;
|
|
293
|
+
/**
|
|
294
|
+
* What kind of claim this result makes, what the composer did with it,
|
|
295
|
+
* which question it answers and which failure classes a failure belongs
|
|
296
|
+
* to — stamped by the engine from the rule's declaration (0.9.0). Absent
|
|
297
|
+
* on results written before that release and on rules that declare no
|
|
298
|
+
* metadata; never fabricated on read.
|
|
299
|
+
*/
|
|
300
|
+
kind?: ClaimKind;
|
|
301
|
+
role?: Role;
|
|
302
|
+
question?: QuestionId;
|
|
303
|
+
classes?: FailureClass[];
|
|
304
|
+
/** The version of the rule definition that produced this result. */
|
|
305
|
+
ruleVersion?: number;
|
|
306
|
+
/** Which of the rule's declared needs the call actually carried — what the rule SAW. */
|
|
307
|
+
saw?: Need[];
|
|
308
|
+
/** Present only when `skipped`; says whether the rule was never asked or was asked and could not answer. */
|
|
309
|
+
skipClass?: SkipClass;
|
|
310
|
+
/** How wrong this result tends to be, and on what basis. Present on every result that made a claim (not on skips). */
|
|
311
|
+
uncertainty?: Uncertainty;
|
|
312
|
+
/** What the rule saw: spans (offsets, never text), tool-call indices, pattern names, counts. Present on every fired detection or inference, and on measurements. */
|
|
313
|
+
evidence?: Evidence[];
|
|
314
|
+
/** A measurement's statistic and unit — the number before the score transform. */
|
|
315
|
+
value?: MeasuredValue;
|
|
69
316
|
/**
|
|
70
317
|
* Deployed rule id (rule-<hex>) when the rule came from the custom-rule
|
|
71
318
|
* store. Absent for built-in rules and for inline custom_rules. Names are
|
|
@@ -169,6 +416,17 @@ export interface EvalResult {
|
|
|
169
416
|
* rule_results carry a `category` per rule so a reader can regroup.
|
|
170
417
|
*/
|
|
171
418
|
categories?: Partial<Record<EvalType, EvalCategoryResult>>;
|
|
419
|
+
/** The verdict with its basis (0.9.0) — computed by the engine, derived on read for stored rows that carry provenance. */
|
|
420
|
+
verdict?: Verdict;
|
|
421
|
+
/** Coverage by evaluation question (0.9.0) — computed by the engine, derived on read from the stamped rule results. */
|
|
422
|
+
coverage?: Coverage;
|
|
423
|
+
/** What produced this verdict (0.9.0) — persisted; absent on rows written before it, never fabricated. */
|
|
424
|
+
provenance?: Provenance;
|
|
425
|
+
/** What the evaluation itself cost (the judge's spend); undefined for the free rules. */
|
|
426
|
+
eval_cost_usd?: number;
|
|
427
|
+
eval_tokens?: number;
|
|
428
|
+
/** Set when the linked trace was deleted (delete_trace or the retention sweep) and this row's text was erased. */
|
|
429
|
+
erased_at?: string;
|
|
172
430
|
}
|
|
173
431
|
export type CustomRuleType = 'regex_match' | 'regex_no_match' | 'min_length' | 'max_length' | 'contains_keywords' | 'excludes_keywords' | 'json_schema' | 'cost_threshold';
|
|
174
432
|
export interface CustomRuleDefinition {
|
package/dist/types/eval.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Evidence lists are capped so a pathological output cannot balloon a stored row. */
|
|
2
|
+
export const MAX_EVIDENCE_ITEMS = 25;
|
package/dist/types/query.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface IStorageAdapter {
|
|
|
82
82
|
getSpansByTraceId(tenantId: TenantId, traceId: string): Promise<Span[]>;
|
|
83
83
|
insertEvalResult(tenantId: TenantId, result: EvalResult): Promise<void>;
|
|
84
84
|
getEvalsByTraceId(tenantId: TenantId, traceId: string): Promise<EvalResult[]>;
|
|
85
|
+
/** One stored evaluation by id, in the same derived-on-read shape as every other reader; null when absent. */
|
|
86
|
+
getEvalById(tenantId: TenantId, id: string): Promise<EvalResult | null>;
|
|
85
87
|
queryEvalResults(tenantId: TenantId, options: {
|
|
86
88
|
eval_type?: string;
|
|
87
89
|
passed?: boolean;
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/iris-eval/mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@iris-eval/mcp-server",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.9.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
2
|
-
export function registerDashboardSummaryResource(server, storage) {
|
|
3
|
-
server.resource('dashboard-summary', 'iris://dashboard/summary', { description: 'Dashboard summary with key metrics and trends' }, async () => {
|
|
4
|
-
// OSS single-tenant: summary scopes to the local user.
|
|
5
|
-
const summary = await storage.getDashboardSummary(LOCAL_TENANT);
|
|
6
|
-
return {
|
|
7
|
-
contents: [
|
|
8
|
-
{
|
|
9
|
-
uri: 'iris://dashboard/summary',
|
|
10
|
-
mimeType: 'application/json',
|
|
11
|
-
text: JSON.stringify(summary, null, 2),
|
|
12
|
-
},
|
|
13
|
-
],
|
|
14
|
-
};
|
|
15
|
-
});
|
|
16
|
-
}
|