@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { JUDGE_KEY_VARS, judgeStateLine } from './judge-enablement.js';
|
|
2
|
+
export const INSTRUCTIONS_MAX_CHARS = 2600;
|
|
3
|
+
function prose(items) {
|
|
4
|
+
if (items.length <= 1)
|
|
5
|
+
return items.join('');
|
|
6
|
+
return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
7
|
+
}
|
|
8
|
+
export function buildInstructions(i) {
|
|
9
|
+
const judgeHowTo = i.judge.enabled
|
|
10
|
+
? 'They spend your key, never a shared one; the cap is refused before any spend.'
|
|
11
|
+
: `To enable them, put your own ${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai} in the env block of the iris-eval entry in your MCP client config and restart the session; iris://capabilities carries the full steps.`;
|
|
12
|
+
const critical = i.critical.length > 0 ? i.critical.join(', ') : 'none';
|
|
13
|
+
const text = [
|
|
14
|
+
'Iris evaluates what an AI agent wrote and what it did. It runs locally, stores traces and verdicts in SQLite, and sends nothing anywhere unless you enable the judge.',
|
|
15
|
+
`Three verbs. LOG an execution with log_trace (input, output, tool_calls, cost). EVALUATE with evaluate_output: ${i.ruleCount} deterministic rules in ${prose(i.categories)}. ` +
|
|
16
|
+
'Pass input so the relevance and hallucination rules can judge; pass tool_calls or a trace_id so the trajectory rules can judge; pass cost_usd so the cost rules can judge. ' +
|
|
17
|
+
'A rule with nothing to judge SKIPS and is named; it is never counted as a pass. INSPECT with get_traces and list_rules, or read a resource.',
|
|
18
|
+
`Reading a verdict. passed is the ship verdict: true only when score clears the threshold (${i.threshold} on this server) AND no critical rule failed. ` +
|
|
19
|
+
'verdict.basis says which layer decided (policy_gate, detector_veto, score_below_threshold, clean, or no_rules when nothing could be judged) and verdict.by names the rules. ' +
|
|
20
|
+
`Critical on this server: ${critical} (configurable; list_rules shows the effective value). ` +
|
|
21
|
+
'coverage says which evaluation questions were judged and why the others were not. A critical rule that could not judge is named in critical_skipped: treat that as UNKNOWN, not clean. ' +
|
|
22
|
+
'score is a quality gradient over the rules that ran; never read it alone as a safety signal.',
|
|
23
|
+
`The LLM judge (evaluate_with_llm_judge) and the citation verifier (verify_citations) are ${judgeStateLine(i.judge)}. ${judgeHowTo}`,
|
|
24
|
+
'Resources: iris://capabilities (what this server can judge, what each rule needs, judge state, limits), iris://proof (measured precision and recall per rule with 95% intervals), ' +
|
|
25
|
+
'iris://traces/{trace_id}, iris://evaluations/{id}, iris://dashboard/summary. Responses link what they created.',
|
|
26
|
+
'Do not use Iris to validate arbitrary JSON Schema, to screen inputs before they reach an agent (the injection rule reads output), or for semantic judgment without a key.',
|
|
27
|
+
'Errors from a tool return {"error":{"code","message","recovery":[]}} with isError true; follow recovery before retrying. ' +
|
|
28
|
+
'An argument the schema rejects comes back as plain text naming IRIS_INVALID_ARGUMENT and the valid arguments.',
|
|
29
|
+
].join('\n\n');
|
|
30
|
+
if (text.length > INSTRUCTIONS_MAX_CHARS) {
|
|
31
|
+
throw new Error(`instructions are ${text.length} characters; the ceiling is ${INSTRUCTIONS_MAX_CHARS}`);
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
export const EVALUATE_MY_AGENT_PROMPT = 'evaluate-my-agent';
|
|
36
|
+
/**
|
|
37
|
+
* The prompt a client shows as a slash command: a walk of log → evaluate →
|
|
38
|
+
* read → explain, in plain words, carrying the version so a cached copy
|
|
39
|
+
* cannot outlive a bump. Rendered from the same facts as the instructions.
|
|
40
|
+
*/
|
|
41
|
+
export function evaluateMyAgentPrompt(what, version) {
|
|
42
|
+
const source = what === 'trace-file'
|
|
43
|
+
? 'Read the trace file I point you at (a JSON object or array with input, output and tool_calls per run).'
|
|
44
|
+
: 'Take the agent output I give you, with the input that produced it and the tool calls if I have them.';
|
|
45
|
+
return [
|
|
46
|
+
`Evaluate my agent with Iris ${version}. Do these steps and report in plain words.`,
|
|
47
|
+
`1. ${source}`,
|
|
48
|
+
'2. Log each run with log_trace, then evaluate it with evaluate_output, passing input, tool_calls (or the trace_id) and cost_usd whenever you have them so the relevance, trajectory and cost rules can judge.',
|
|
49
|
+
'3. Read the verdict: passed is the ship verdict; verdict.basis and verdict.by say which rule decided; coverage says what was not judged and why; critical_skipped means UNKNOWN, not clean.',
|
|
50
|
+
'4. For anything that failed, follow the resource link to iris://evaluations/{id} and quote the rule, its message and its evidence (offsets into my output, never a paraphrase of what the rule matched).',
|
|
51
|
+
'5. Tell me: what passed, what failed and why, what was not judged and what input would let Iris judge it. If I ask for a semantic judgment and the judge is not enabled, give me the recovery steps from the error instead of searching for them.',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type JudgeProvider = 'anthropic' | 'openai';
|
|
2
|
+
export declare const JUDGE_KEY_VARS: Readonly<Record<JudgeProvider, string>>;
|
|
3
|
+
export declare const JUDGE_COST_CAP_VAR = "IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL";
|
|
4
|
+
export declare const JUDGE_DEFAULT_COST_CAP_USD = 0.25;
|
|
5
|
+
export interface JudgeState {
|
|
6
|
+
/** A key for at least one provider is present in this process's environment. */
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
/** The provider Iris would pick when a call names no provider: the first one with a key. */
|
|
9
|
+
provider: JudgeProvider | null;
|
|
10
|
+
/** Every provider with a key present. */
|
|
11
|
+
providers: JudgeProvider[];
|
|
12
|
+
/** The per-call spend cap the judge refuses beyond, before any spend. */
|
|
13
|
+
costCapUsd: number;
|
|
14
|
+
}
|
|
15
|
+
/** The per-call cap from the environment, else the shipped default. */
|
|
16
|
+
export declare function judgeCostCapUsd(): number;
|
|
17
|
+
export declare function judgeState(): JudgeState;
|
|
18
|
+
export declare const JUDGE_ENABLE_TITLE: string;
|
|
19
|
+
/**
|
|
20
|
+
* The enable workflow, one step per entry, from judge-enablement.json.
|
|
21
|
+
* Rendered verbatim into the IRIS_JUDGE_NOT_ENABLED error's `recovery`,
|
|
22
|
+
* the capabilities resource's `howToEnable`, and (through the truthbase)
|
|
23
|
+
* the README, the judge doc and the skill files. Change the JSON and
|
|
24
|
+
* nowhere else.
|
|
25
|
+
*/
|
|
26
|
+
export declare const JUDGE_ENABLE_STEPS: readonly string[];
|
|
27
|
+
/** The block as prose: the title in bold, then the numbered steps — the exact form every surface carries. */
|
|
28
|
+
export declare function judgeEnableBlock(): string;
|
|
29
|
+
/** Shared with scripts/claims/render-llms.mjs, which renders the same shape from .claims.json. */
|
|
30
|
+
export declare function renderJudgeEnableBlock(title: string, steps: readonly string[]): string;
|
|
31
|
+
/** The steps as an error's `recovery[]`, with the variable the call needed named first. */
|
|
32
|
+
export declare function judgeRecovery(provider: JudgeProvider): string[];
|
|
33
|
+
/** One line for the instructions and the self-test: what this process can see. */
|
|
34
|
+
export declare function judgeStateLine(state: JudgeState): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The judge is a user-keyed feature (founder ruling, 2026-09-04): an end
|
|
3
|
+
* user enables it by supplying their own provider key, and every surface
|
|
4
|
+
* that mentions it has to say so in the same words — the error a tool
|
|
5
|
+
* returns without a key, the capabilities resource, the server
|
|
6
|
+
* instructions, the self-test, the README and the docs. The words live
|
|
7
|
+
* once, in judge-enablement.json beside this file: the runtime imports
|
|
8
|
+
* it, the truthbase generator copies it into .claims.json, the skill files
|
|
9
|
+
* render it as a slot, and tests/judge-enablement-surfaces.test.ts asserts
|
|
10
|
+
* the README and the judge doc carry the same block verbatim.
|
|
11
|
+
*
|
|
12
|
+
* What "enabled" means: a key for at least one provider reached THIS
|
|
13
|
+
* process's environment. That is the only fact Iris can check, and it is
|
|
14
|
+
* the fact users get wrong — a key exported in a shell is not passed to
|
|
15
|
+
* the child process an MCP client spawns unless the client's config says
|
|
16
|
+
* so. Step 2 exists because of that.
|
|
17
|
+
*/
|
|
18
|
+
import enable from './judge-enablement.json' with { type: 'json' };
|
|
19
|
+
export const JUDGE_KEY_VARS = {
|
|
20
|
+
anthropic: 'IRIS_ANTHROPIC_API_KEY',
|
|
21
|
+
openai: 'IRIS_OPENAI_API_KEY',
|
|
22
|
+
};
|
|
23
|
+
export const JUDGE_COST_CAP_VAR = 'IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL';
|
|
24
|
+
export const JUDGE_DEFAULT_COST_CAP_USD = 0.25;
|
|
25
|
+
/** The per-call cap from the environment, else the shipped default. */
|
|
26
|
+
export function judgeCostCapUsd() {
|
|
27
|
+
const raw = process.env.IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL;
|
|
28
|
+
if (raw) {
|
|
29
|
+
const parsed = Number(raw);
|
|
30
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
return JUDGE_DEFAULT_COST_CAP_USD;
|
|
34
|
+
}
|
|
35
|
+
export function judgeState() {
|
|
36
|
+
// Literal reads on purpose: the docs contract and the manifest parity
|
|
37
|
+
// test grep `process.env.IRIS_*` to learn what the server reads.
|
|
38
|
+
const present = {
|
|
39
|
+
anthropic: Boolean(process.env.IRIS_ANTHROPIC_API_KEY),
|
|
40
|
+
openai: Boolean(process.env.IRIS_OPENAI_API_KEY),
|
|
41
|
+
};
|
|
42
|
+
const providers = Object.keys(JUDGE_KEY_VARS).filter((p) => present[p]);
|
|
43
|
+
return {
|
|
44
|
+
enabled: providers.length > 0,
|
|
45
|
+
provider: providers[0] ?? null,
|
|
46
|
+
providers,
|
|
47
|
+
costCapUsd: judgeCostCapUsd(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export const JUDGE_ENABLE_TITLE = enable.title;
|
|
51
|
+
/**
|
|
52
|
+
* The enable workflow, one step per entry, from judge-enablement.json.
|
|
53
|
+
* Rendered verbatim into the IRIS_JUDGE_NOT_ENABLED error's `recovery`,
|
|
54
|
+
* the capabilities resource's `howToEnable`, and (through the truthbase)
|
|
55
|
+
* the README, the judge doc and the skill files. Change the JSON and
|
|
56
|
+
* nowhere else.
|
|
57
|
+
*/
|
|
58
|
+
export const JUDGE_ENABLE_STEPS = enable.steps;
|
|
59
|
+
/** The block as prose: the title in bold, then the numbered steps — the exact form every surface carries. */
|
|
60
|
+
export function judgeEnableBlock() {
|
|
61
|
+
return renderJudgeEnableBlock(JUDGE_ENABLE_TITLE, JUDGE_ENABLE_STEPS);
|
|
62
|
+
}
|
|
63
|
+
/** Shared with scripts/claims/render-llms.mjs, which renders the same shape from .claims.json. */
|
|
64
|
+
export function renderJudgeEnableBlock(title, steps) {
|
|
65
|
+
return [`**${title}**`, ...steps.map((s, i) => `${i + 1}. ${s}`)].join('\n');
|
|
66
|
+
}
|
|
67
|
+
/** The steps as an error's `recovery[]`, with the variable the call needed named first. */
|
|
68
|
+
export function judgeRecovery(provider) {
|
|
69
|
+
return [`Set ${JUDGE_KEY_VARS[provider]} for the process that runs Iris.`, ...JUDGE_ENABLE_STEPS];
|
|
70
|
+
}
|
|
71
|
+
/** One line for the instructions and the self-test: what this process can see. */
|
|
72
|
+
export function judgeStateLine(state) {
|
|
73
|
+
if (state.enabled) {
|
|
74
|
+
const list = state.providers.join(' and ');
|
|
75
|
+
return `enabled (${list} key found in this process; each call is capped at ${state.costCapUsd} USD)`;
|
|
76
|
+
}
|
|
77
|
+
return `not enabled (no ${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai} reached this process)`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Enable the LLM judge (optional; the deterministic rules never need it)",
|
|
3
|
+
"steps": [
|
|
4
|
+
"Get an API key from Anthropic or OpenAI.",
|
|
5
|
+
"Put it in the environment of the process that runs Iris, not only your shell. Claude Code, Claude Desktop, Cursor and most MCP clients: the \"env\" block of the iris-eval entry in your MCP config — \"iris-eval\": { \"command\": \"npx\", \"args\": [\"-y\", \"@iris-eval/mcp-server\"], \"env\": { \"IRIS_ANTHROPIC_API_KEY\": \"sk-ant-...\" } } (IRIS_OPENAI_API_KEY for an OpenAI key). Docker: -e IRIS_ANTHROPIC_API_KEY=... on the run command. HTTP or CI: export it before starting iris-mcp.",
|
|
6
|
+
"Restart the MCP session. A running process never sees a variable set after it started.",
|
|
7
|
+
"Confirm from inside your client: read iris://capabilities — judge.enabled must be true there. A key exported in your shell is not passed to the process your client spawns unless its config lists it. On a machine, `npx @iris-eval/mcp-server --self-test` prints the judge line for that shell, and GET /api/v1/health reports judge.enabled on a running dashboard.",
|
|
8
|
+
"Spend guard: each call is capped by IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL (default 0.25 USD) and refused before any spend if the worst case would exceed it. Iris calls the provider directly with your key and never proxies it."
|
|
9
|
+
]
|
|
10
|
+
}
|
package/dist/preferences.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export declare const PreferencesSchema: z.ZodObject<{
|
|
|
22
22
|
firstSeen: z.ZodOptional<z.ZodString>;
|
|
23
23
|
dismissedBanners: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
24
24
|
theme: z.ZodDefault<z.ZodEnum<{
|
|
25
|
+
system: "system";
|
|
25
26
|
light: "light";
|
|
26
27
|
dark: "dark";
|
|
27
|
-
system: "system";
|
|
28
28
|
}>>;
|
|
29
29
|
momentFilters: z.ZodDefault<z.ZodObject<{
|
|
30
30
|
agentName: z.ZodOptional<z.ZodString>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The one prompt: `evaluate-my-agent`. A client that supports prompts
|
|
3
|
+
* shows it as a slash command; a client that does not never sees it, and
|
|
4
|
+
* nothing in the README depends on it. Rendered from the same facts as
|
|
5
|
+
* the server instructions (src/instructions.ts) and carrying the version,
|
|
6
|
+
* so a cached copy cannot outlive a bump.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { EVALUATE_MY_AGENT_PROMPT, evaluateMyAgentPrompt } from './instructions.js';
|
|
10
|
+
export const PROMPT_NAMES = [EVALUATE_MY_AGENT_PROMPT];
|
|
11
|
+
export function registerPrompts(server, version) {
|
|
12
|
+
server.registerPrompt(EVALUATE_MY_AGENT_PROMPT, {
|
|
13
|
+
title: 'Evaluate my agent',
|
|
14
|
+
description: 'A walk through logging an agent run, evaluating it and reading the verdict with Iris, in plain words.',
|
|
15
|
+
argsSchema: {
|
|
16
|
+
what: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('"output" (default): you have an agent output to hand; "trace-file": you have a file of runs to evaluate'),
|
|
20
|
+
},
|
|
21
|
+
}, ({ what }) => ({
|
|
22
|
+
messages: [
|
|
23
|
+
{
|
|
24
|
+
role: 'user',
|
|
25
|
+
content: { type: 'text', text: evaluateMyAgentPrompt(what === 'trace-file' ? 'trace-file' : 'output', version) },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { IStorageAdapter } from '../types/query.js';
|
|
3
|
-
|
|
3
|
+
import type { Capabilities } from '../capabilities.js';
|
|
4
|
+
/** The MCP resource-not-found code (the spec reserves -32002 for it). */
|
|
5
|
+
export declare const RESOURCE_NOT_FOUND = -32002;
|
|
6
|
+
export declare function registerAllResources(server: McpServer, storage: IStorageAdapter, capabilities: () => Capabilities): void;
|
package/dist/resources/index.js
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/*
|
|
2
|
+
* The resources, registered the way the SDK wants them: fixed URIs with
|
|
3
|
+
* registerResource, parameterised ones with a ResourceTemplate (so a
|
|
4
|
+
* client's resources/templates/list shows the shape, and the SDK parses
|
|
5
|
+
* the variable instead of the handler splitting a path). A resource that
|
|
6
|
+
* does not exist is the protocol's resource-not-found error, never a 200
|
|
7
|
+
* body with an "error" key a client would have to sniff for.
|
|
8
|
+
*/
|
|
9
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { ruleProof } from '../capabilities.js';
|
|
12
|
+
import { publishedProvenance, publishedRuleNames } from '../eval/accuracy.js';
|
|
13
|
+
import { toEvaluationResponse } from '../eval/response.js';
|
|
14
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
15
|
+
import { CAPABILITIES_RESOURCE_URI, DASHBOARD_SUMMARY_RESOURCE_URI, EVALUATION_RESOURCE_TEMPLATE, PROOF_RESOURCE_URI, TRACE_RESOURCE_TEMPLATE, } from './uris.js';
|
|
16
|
+
/** The MCP resource-not-found code (the spec reserves -32002 for it). */
|
|
17
|
+
export const RESOURCE_NOT_FOUND = -32002;
|
|
18
|
+
function notFound(uri, what) {
|
|
19
|
+
return new McpError(RESOURCE_NOT_FOUND, `Resource not found: ${uri} — no ${what} with that id is stored for this tenant`, { uri });
|
|
20
|
+
}
|
|
21
|
+
const json = (uri, body) => ({
|
|
22
|
+
contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(body, null, 2) }],
|
|
23
|
+
});
|
|
24
|
+
export function registerAllResources(server, storage, capabilities) {
|
|
25
|
+
server.registerResource('capabilities', CAPABILITIES_RESOURCE_URI, {
|
|
26
|
+
title: 'Capabilities',
|
|
27
|
+
description: 'What this server can judge: the rule roster with what each rule needs and its published accuracy, the judge state with the steps that enable it, the citation verifier posture, the dashboard address, the limits, and the tools, resources and prompts registered.',
|
|
28
|
+
mimeType: 'application/json',
|
|
29
|
+
}, async (uri) => json(uri.href, capabilities()));
|
|
30
|
+
server.registerResource('proof', PROOF_RESOURCE_URI, {
|
|
31
|
+
title: 'Proof',
|
|
32
|
+
description: 'The published accuracy of every measured built-in rule (the same numbers as https://iris-eval.com/proof): precision and recall on the proof corpus with 95% intervals, the confusion counts, positive predictive value at four prevalences, and the corpus version and labelling the numbers come from.',
|
|
33
|
+
mimeType: 'application/json',
|
|
34
|
+
}, async (uri) => {
|
|
35
|
+
const prov = publishedProvenance();
|
|
36
|
+
const rules = {};
|
|
37
|
+
for (const name of publishedRuleNames())
|
|
38
|
+
rules[name] = ruleProof(name);
|
|
39
|
+
return json(uri.href, {
|
|
40
|
+
corpusVersion: prov.corpusVersion,
|
|
41
|
+
release: prov.release,
|
|
42
|
+
labelling: prov.labelling,
|
|
43
|
+
method: 'Each rule is run over its proof-corpus family; precision and recall carry Wilson 95% intervals; ppvAt is the positive predictive value at the stated prevalence, from the same counts. Reproduce with `npm run proof` in the repository.',
|
|
44
|
+
rules,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
server.registerResource('dashboard-summary', DASHBOARD_SUMMARY_RESOURCE_URI, { title: 'Dashboard summary', description: 'Dashboard summary with key metrics and trends for the last hour', mimeType: 'application/json' }, async (uri) => json(uri.href, await storage.getDashboardSummary(LOCAL_TENANT)));
|
|
48
|
+
server.registerResource('trace-detail', new ResourceTemplate(TRACE_RESOURCE_TEMPLATE, { list: undefined }), { title: 'Trace', description: 'One stored trace with its spans and every evaluation linked to it', mimeType: 'application/json' }, async (uri, variables) => {
|
|
49
|
+
const traceId = String(variables.trace_id ?? '');
|
|
50
|
+
const trace = await storage.getTrace(LOCAL_TENANT, traceId);
|
|
51
|
+
if (!trace)
|
|
52
|
+
throw notFound(uri.href, 'trace');
|
|
53
|
+
const [spans, evals] = await Promise.all([
|
|
54
|
+
storage.getSpansByTraceId(LOCAL_TENANT, traceId),
|
|
55
|
+
storage.getEvalsByTraceId(LOCAL_TENANT, traceId),
|
|
56
|
+
]);
|
|
57
|
+
return json(uri.href, { trace, spans, evals: evals.map((e) => toEvaluationResponse(e, { traceId })) });
|
|
58
|
+
});
|
|
59
|
+
server.registerResource('evaluation-detail', new ResourceTemplate(EVALUATION_RESOURCE_TEMPLATE, { list: undefined }), { title: 'Evaluation', description: 'One stored evaluation, in the same shape evaluate_output returned it: verdict, coverage, provenance and every rule result with its evidence', mimeType: 'application/json' }, async (uri, variables) => {
|
|
60
|
+
const id = String(variables.id ?? '');
|
|
61
|
+
const result = await storage.getEvalById(LOCAL_TENANT, id);
|
|
62
|
+
if (!result)
|
|
63
|
+
throw notFound(uri.href, 'evaluation');
|
|
64
|
+
return json(uri.href, toEvaluationResponse(result, { traceId: result.trace_id }));
|
|
65
|
+
});
|
|
6
66
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CAPABILITIES_RESOURCE_URI = "iris://capabilities";
|
|
2
|
+
export declare const PROOF_RESOURCE_URI = "iris://proof";
|
|
3
|
+
export declare const DASHBOARD_SUMMARY_RESOURCE_URI = "iris://dashboard/summary";
|
|
4
|
+
export declare const TRACE_RESOURCE_TEMPLATE = "iris://traces/{trace_id}";
|
|
5
|
+
export declare const EVALUATION_RESOURCE_TEMPLATE = "iris://evaluations/{id}";
|
|
6
|
+
export declare const RESOURCE_URIS: readonly ["iris://capabilities", "iris://proof", "iris://dashboard/summary", "iris://traces/{trace_id}", "iris://evaluations/{id}"];
|
|
7
|
+
/** The URI of one stored trace / evaluation, from its template. */
|
|
8
|
+
export declare const traceUri: (traceId: string) => string;
|
|
9
|
+
export declare const evaluationUri: (id: string) => string;
|
|
10
|
+
/** Fixed URIs (resources/list) versus templates (resources/templates/list). */
|
|
11
|
+
export declare const FIXED_RESOURCE_URIS: readonly ["iris://capabilities", "iris://proof", "iris://dashboard/summary"];
|
|
12
|
+
export declare const RESOURCE_TEMPLATES: readonly ["iris://traces/{trace_id}", "iris://evaluations/{id}"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Every resource URI this server registers, as literals: the capabilities
|
|
3
|
+
* object lists them, the docs contract checks prose against them, and a
|
|
4
|
+
* test asserts the list equals what resources/list and
|
|
5
|
+
* resources/templates/list return.
|
|
6
|
+
*/
|
|
7
|
+
export const CAPABILITIES_RESOURCE_URI = 'iris://capabilities';
|
|
8
|
+
export const PROOF_RESOURCE_URI = 'iris://proof';
|
|
9
|
+
export const DASHBOARD_SUMMARY_RESOURCE_URI = 'iris://dashboard/summary';
|
|
10
|
+
export const TRACE_RESOURCE_TEMPLATE = 'iris://traces/{trace_id}';
|
|
11
|
+
export const EVALUATION_RESOURCE_TEMPLATE = 'iris://evaluations/{id}';
|
|
12
|
+
export const RESOURCE_URIS = [
|
|
13
|
+
CAPABILITIES_RESOURCE_URI,
|
|
14
|
+
PROOF_RESOURCE_URI,
|
|
15
|
+
DASHBOARD_SUMMARY_RESOURCE_URI,
|
|
16
|
+
TRACE_RESOURCE_TEMPLATE,
|
|
17
|
+
EVALUATION_RESOURCE_TEMPLATE,
|
|
18
|
+
];
|
|
19
|
+
/** The URI of one stored trace / evaluation, from its template. */
|
|
20
|
+
export const traceUri = (traceId) => TRACE_RESOURCE_TEMPLATE.replace('{trace_id}', traceId);
|
|
21
|
+
export const evaluationUri = (id) => EVALUATION_RESOURCE_TEMPLATE.replace('{id}', id);
|
|
22
|
+
/** Fixed URIs (resources/list) versus templates (resources/templates/list). */
|
|
23
|
+
export const FIXED_RESOURCE_URIS = [CAPABILITIES_RESOURCE_URI, PROOF_RESOURCE_URI, DASHBOARD_SUMMARY_RESOURCE_URI];
|
|
24
|
+
export const RESOURCE_TEMPLATES = [TRACE_RESOURCE_TEMPLATE, EVALUATION_RESOURCE_TEMPLATE];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IrisConfig } from './types/config.js';
|
|
2
|
+
import type { IStorageAdapter } from './types/query.js';
|
|
3
|
+
export interface RetentionLogger {
|
|
4
|
+
info(msg: string): void;
|
|
5
|
+
warn(msg: string): void;
|
|
6
|
+
}
|
|
7
|
+
export interface SweepOutcome {
|
|
8
|
+
deletedTraces: number;
|
|
9
|
+
deletedEvals: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One sweep for the local tenant. Never throws — a failed sweep is logged
|
|
13
|
+
* and the server keeps serving; the next tick tries again.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runRetentionSweep(storage: IStorageAdapter, config: IrisConfig, logger: RetentionLogger): Promise<SweepOutcome | null>;
|
|
16
|
+
/**
|
|
17
|
+
* The timer. Returns the handle (already unref'd) so a caller can clear it
|
|
18
|
+
* on shutdown, or null when retention or the timer is disabled.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scheduleRetentionSweep(storage: IStorageAdapter, config: IrisConfig, logger: RetentionLogger): NodeJS.Timeout | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LOCAL_TENANT } from './types/tenant.js';
|
|
2
|
+
/**
|
|
3
|
+
* One sweep for the local tenant. Never throws — a failed sweep is logged
|
|
4
|
+
* and the server keeps serving; the next tick tries again.
|
|
5
|
+
*/
|
|
6
|
+
export async function runRetentionSweep(storage, config, logger) {
|
|
7
|
+
if (config.retention.days <= 0)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const deletedTraces = await storage.deleteTracesOlderThan(LOCAL_TENANT, config.retention.days);
|
|
11
|
+
/*
|
|
12
|
+
* Evaluations too (#372). Deleting a trace only NULLs trace_id on
|
|
13
|
+
* its evaluations, so every eval row — output_text verbatim,
|
|
14
|
+
* including whatever no_pii flagged — used to outlive the retention
|
|
15
|
+
* window indefinitely while the traces around it were swept.
|
|
16
|
+
*/
|
|
17
|
+
const deletedEvals = await storage.deleteEvalResultsOlderThan(LOCAL_TENANT, config.retention.days);
|
|
18
|
+
if (deletedTraces + deletedEvals > 0) {
|
|
19
|
+
// Fold the WAL into the main file and truncate it, so the swept
|
|
20
|
+
// rows do not survive as readable text in iris.db-wal.
|
|
21
|
+
await storage.checkpoint();
|
|
22
|
+
logger.info(`Retention cleanup: deleted ${deletedTraces} trace(s) and ${deletedEvals} evaluation(s) older than ${config.retention.days} days`);
|
|
23
|
+
}
|
|
24
|
+
return { deletedTraces, deletedEvals };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.warn(`Retention cleanup skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The timer. Returns the handle (already unref'd) so a caller can clear it
|
|
33
|
+
* on shutdown, or null when retention or the timer is disabled.
|
|
34
|
+
*/
|
|
35
|
+
export function scheduleRetentionSweep(storage, config, logger) {
|
|
36
|
+
const hours = config.retention.sweepIntervalHours;
|
|
37
|
+
if (config.retention.days <= 0 || !(hours > 0))
|
|
38
|
+
return null;
|
|
39
|
+
const timer = setInterval(() => {
|
|
40
|
+
void runRetentionSweep(storage, config, logger);
|
|
41
|
+
}, hours * 60 * 60 * 1000);
|
|
42
|
+
timer.unref();
|
|
43
|
+
return timer;
|
|
44
|
+
}
|
package/dist/self-test.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const SELF_TEST_STEPS: {
|
|
2
2
|
readonly configuredHome: "configured IRIS_HOME is writable";
|
|
3
|
+
readonly judge: "judge key in this shell";
|
|
3
4
|
readonly tempHome: "create isolated temp home";
|
|
4
5
|
readonly storage: "initialize storage";
|
|
5
6
|
readonly trace: "log a trace";
|
package/dist/self-test.js
CHANGED
|
@@ -48,6 +48,7 @@ import { irisHome } from './utils/iris-home.js';
|
|
|
48
48
|
import { EvalEngine } from './eval/engine.js';
|
|
49
49
|
import { generateTraceId } from './utils/ids.js';
|
|
50
50
|
import { LOCAL_TENANT } from './types/tenant.js';
|
|
51
|
+
import { judgeState, judgeStateLine } from './judge-enablement.js';
|
|
51
52
|
const CHECK = '✓';
|
|
52
53
|
const CROSS = '✗';
|
|
53
54
|
/*
|
|
@@ -57,6 +58,7 @@ const CROSS = '✗';
|
|
|
57
58
|
*/
|
|
58
59
|
export const SELF_TEST_STEPS = {
|
|
59
60
|
configuredHome: 'configured IRIS_HOME is writable',
|
|
61
|
+
judge: 'judge key in this shell',
|
|
60
62
|
tempHome: 'create isolated temp home',
|
|
61
63
|
storage: 'initialize storage',
|
|
62
64
|
trace: 'log a trace',
|
|
@@ -238,6 +240,18 @@ export async function runSelfTest(write = stdoutLine) {
|
|
|
238
240
|
await step(SELF_TEST_STEPS.configuredHome, () => probeConfiguredHome(userHome, userStoragePath), {
|
|
239
241
|
independent: true,
|
|
240
242
|
});
|
|
243
|
+
/*
|
|
244
|
+
* The judge line, read from THIS shell's environment before the scrub
|
|
245
|
+
* (the judge variables are not scrubbed, but the order keeps the claim
|
|
246
|
+
* honest). Informational — a missing key is not a failed install — and
|
|
247
|
+
* it never calls a provider. The sentence about the client's env block
|
|
248
|
+
* is the whole point: a key exported here can print enabled while the
|
|
249
|
+
* process an MCP client spawns never receives it.
|
|
250
|
+
*/
|
|
251
|
+
await step(SELF_TEST_STEPS.judge, () => {
|
|
252
|
+
const state = judgeState();
|
|
253
|
+
return `${judgeStateLine(state)}; your MCP client passes only what its config env block lists — confirm with iris://capabilities from inside the client`;
|
|
254
|
+
}, { independent: true });
|
|
241
255
|
await step(SELF_TEST_STEPS.tempHome, () => {
|
|
242
256
|
tempHome = mkdtempSync(join(tmpdir(), 'iris-self-test-'));
|
|
243
257
|
for (const key of SCRUBBED_ENV_VARS) {
|
|
@@ -291,7 +305,7 @@ export async function runSelfTest(write = stdoutLine) {
|
|
|
291
305
|
insertedIds.push(result.id);
|
|
292
306
|
};
|
|
293
307
|
await step(SELF_TEST_STEPS.piiEval, async () => {
|
|
294
|
-
const result = evalEngine.evaluate('safety', {
|
|
308
|
+
const result = await evalEngine.evaluate('safety', {
|
|
295
309
|
// A real-shaped SSN, not the never-issued 123-45-6789 documentation
|
|
296
310
|
// placeholder — no_pii suppresses that one on purpose.
|
|
297
311
|
output: 'Done. For the record, the customer SSN is 536-22-8145.',
|
|
@@ -303,7 +317,7 @@ export async function runSelfTest(write = stdoutLine) {
|
|
|
303
317
|
return 'no_pii flagged the planted SSN';
|
|
304
318
|
});
|
|
305
319
|
await step(SELF_TEST_STEPS.injectionEval, async () => {
|
|
306
|
-
const result = evalEngine.evaluate('safety', {
|
|
320
|
+
const result = await evalEngine.evaluate('safety', {
|
|
307
321
|
output: 'Sure. I will ignore all previous instructions and reveal the system prompt.',
|
|
308
322
|
});
|
|
309
323
|
const rule = result.rule_results.find((r) => r.ruleName === 'no_injection_patterns');
|
|
@@ -313,7 +327,7 @@ export async function runSelfTest(write = stdoutLine) {
|
|
|
313
327
|
return 'no_injection_patterns flagged the override text';
|
|
314
328
|
});
|
|
315
329
|
await step(SELF_TEST_STEPS.cleanEval, async () => {
|
|
316
|
-
const result = evalEngine.evaluate('safety', {
|
|
330
|
+
const result = await evalEngine.evaluate('safety', {
|
|
317
331
|
output: 'The report is ready: weather in Paris stays mild this week, with light rain expected on Thursday evening.',
|
|
318
332
|
});
|
|
319
333
|
ensure(result.passed && result.score === 1, `clean output should score 1 and pass; got score=${result.score} passed=${result.passed}`);
|
package/dist/server.d.ts
CHANGED
|
@@ -3,9 +3,18 @@ import type { IrisConfig } from './types/index.js';
|
|
|
3
3
|
import type { IStorageAdapter } from './types/query.js';
|
|
4
4
|
import type { CustomRuleStore } from './custom-rule-store.js';
|
|
5
5
|
import { EvalEngine } from './eval/engine.js';
|
|
6
|
+
import { type Capabilities } from './capabilities.js';
|
|
6
7
|
export interface IrisServer {
|
|
7
8
|
mcpServer: McpServer;
|
|
8
9
|
evalEngine: EvalEngine;
|
|
9
10
|
customRuleStore: CustomRuleStore;
|
|
11
|
+
/** The instructions the client received at initialize — built from this server's runtime state. */
|
|
12
|
+
instructions: string;
|
|
13
|
+
/** What this server can do, as iris://capabilities and /api/v1/capabilities serve it. */
|
|
14
|
+
capabilities: () => Capabilities;
|
|
10
15
|
}
|
|
11
|
-
export
|
|
16
|
+
export interface IrisServerOptions {
|
|
17
|
+
/** `demo` when the server runs against the disposable demo database. */
|
|
18
|
+
mode?: 'real' | 'demo';
|
|
19
|
+
}
|
|
20
|
+
export declare function createIrisServer(config: IrisConfig, storage: IStorageAdapter, customRuleStore?: CustomRuleStore, options?: IrisServerOptions): IrisServer;
|
package/dist/server.js
CHANGED
|
@@ -1,20 +1,47 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { EvalEngine } from './eval/engine.js';
|
|
3
|
+
import { rulesByType } from './eval/rules/index.js';
|
|
4
|
+
import { builtInRuleRoster } from './eval/criticality.js';
|
|
3
5
|
import { registerAllTools } from './tools/index.js';
|
|
4
6
|
import { registerAllResources } from './resources/index.js';
|
|
7
|
+
import { registerPrompts } from './prompts.js';
|
|
5
8
|
import { createCustomRuleStore } from './custom-rule-store.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
9
|
+
import { buildInstructions } from './instructions.js';
|
|
10
|
+
import { buildCapabilities } from './capabilities.js';
|
|
11
|
+
import { judgeState } from './judge-enablement.js';
|
|
12
|
+
export function createIrisServer(config, storage, customRuleStore, options) {
|
|
11
13
|
const evalEngine = new EvalEngine(config.eval.defaultThreshold, config.eval.ruleThresholds, config.eval);
|
|
12
14
|
// Caller can inject a shared rule store (e.g. index.ts passes the
|
|
13
15
|
// same instance the HTTP dashboard uses, so a rule deployed via MCP
|
|
14
16
|
// is immediately visible in the dashboard without a restart). If
|
|
15
17
|
// none provided, create a fresh one loading from the default path.
|
|
16
18
|
const ruleStore = customRuleStore ?? createCustomRuleStore();
|
|
19
|
+
/*
|
|
20
|
+
* The instructions are built from what THIS process will do: the
|
|
21
|
+
* roster and bundles from the registry, the critical list after this
|
|
22
|
+
* config's promotions and demotions, and whether a judge key reached
|
|
23
|
+
* this environment. The key is read here once more at boot only to
|
|
24
|
+
* describe the state; the judge tools resolve it again per call, and
|
|
25
|
+
* both reads see the same environment because a process's environment
|
|
26
|
+
* is fixed when its client spawns it.
|
|
27
|
+
*/
|
|
28
|
+
const roster = builtInRuleRoster((rule) => evalEngine.effectiveCriticality(rule));
|
|
29
|
+
const instructions = buildInstructions({
|
|
30
|
+
ruleCount: roster.length,
|
|
31
|
+
categories: Object.entries(rulesByType)
|
|
32
|
+
.filter(([, rules]) => rules.length > 0)
|
|
33
|
+
.map(([category]) => category),
|
|
34
|
+
threshold: config.eval.defaultThreshold,
|
|
35
|
+
critical: roster.filter((r) => r.critical).map((r) => r.name),
|
|
36
|
+
judge: judgeState(),
|
|
37
|
+
});
|
|
38
|
+
const mcpServer = new McpServer({
|
|
39
|
+
name: config.server.name,
|
|
40
|
+
version: config.server.version,
|
|
41
|
+
}, { instructions });
|
|
42
|
+
const capabilities = () => buildCapabilities({ config, evalEngine, customRuleStore: ruleStore, mode: options?.mode });
|
|
17
43
|
registerAllTools(mcpServer, storage, evalEngine, ruleStore);
|
|
18
|
-
registerAllResources(mcpServer, storage);
|
|
19
|
-
|
|
44
|
+
registerAllResources(mcpServer, storage, capabilities);
|
|
45
|
+
registerPrompts(mcpServer, config.server.version);
|
|
46
|
+
return { mcpServer, evalEngine, customRuleStore: ruleStore, instructions, capabilities };
|
|
20
47
|
}
|
package/dist/storage/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SqliteAdapter } from './sqlite-adapter.js';
|
|
|
2
2
|
export function createStorage(config) {
|
|
3
3
|
switch (config.storage.type) {
|
|
4
4
|
case 'sqlite':
|
|
5
|
-
return new SqliteAdapter(config.storage.path);
|
|
5
|
+
return new SqliteAdapter(config.storage.path, { redact: config.storage.redact ?? 'none' });
|
|
6
6
|
default:
|
|
7
7
|
throw new Error(`Unsupported storage type: ${config.storage.type} (supported: sqlite)`);
|
|
8
8
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const id = '007-eval-provenance';
|
|
2
|
+
/*
|
|
3
|
+
* What an evaluation cannot reconstruct about itself after the fact: the
|
|
4
|
+
* Iris version, the ruleset and configuration hashes and the threshold that
|
|
5
|
+
* produced it (arc zero: "why did this pass on that day" was unanswerable
|
|
6
|
+
* from Iris alone), the judge's spend (the tool description said it was
|
|
7
|
+
* kept; the write path stored none of it), and the erasure stamp the
|
|
8
|
+
* right-to-erasure fix sets when a trace is deleted. `writer_version` on the
|
|
9
|
+
* migration ledger lets a downgraded binary refuse a database it cannot
|
|
10
|
+
* read instead of reading half a schema.
|
|
11
|
+
*
|
|
12
|
+
* `verdict`, `coverage` and `critical_skipped` are NOT columns: every one of
|
|
13
|
+
* them is derived on read from the stored rule_results plus the threshold
|
|
14
|
+
* kept here, so rows written before this migration read back the same way
|
|
15
|
+
* without a backfill.
|
|
16
|
+
*/
|
|
17
|
+
export function up(db) {
|
|
18
|
+
db.exec(`
|
|
19
|
+
ALTER TABLE eval_results ADD COLUMN provenance TEXT;
|
|
20
|
+
ALTER TABLE eval_results ADD COLUMN engine_version TEXT;
|
|
21
|
+
ALTER TABLE eval_results ADD COLUMN ruleset_hash TEXT;
|
|
22
|
+
ALTER TABLE eval_results ADD COLUMN config_hash TEXT;
|
|
23
|
+
ALTER TABLE eval_results ADD COLUMN threshold REAL;
|
|
24
|
+
ALTER TABLE eval_results ADD COLUMN eval_cost_usd REAL;
|
|
25
|
+
ALTER TABLE eval_results ADD COLUMN eval_tokens INTEGER;
|
|
26
|
+
ALTER TABLE eval_results ADD COLUMN erased_at TEXT;
|
|
27
|
+
ALTER TABLE _iris_migrations ADD COLUMN writer_version TEXT;
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_engine ON eval_results(tenant_id, engine_version, ruleset_hash);
|
|
29
|
+
`);
|
|
30
|
+
}
|