@iris-eval/mcp-server 0.3.0 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/audit-log-reader.d.ts +24 -0
- package/dist/audit-log-reader.js +87 -0
- package/dist/config/defaults.js +7 -1
- package/dist/custom-rule-store.d.ts +27 -0
- package/dist/custom-rule-store.js +188 -0
- package/dist/dashboard/assets/index-BEG5FYWH.css +1 -0
- package/dist/dashboard/assets/index-D9JHfSB2.js +12 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/audit.d.ts +3 -0
- package/dist/dashboard/routes/audit.js +36 -0
- package/dist/dashboard/routes/eval-stats.js +9 -4
- package/dist/dashboard/routes/evaluations.js +3 -1
- package/dist/dashboard/routes/filters.js +5 -3
- package/dist/dashboard/routes/health.js +8 -1
- package/dist/dashboard/routes/index.d.ts +4 -0
- package/dist/dashboard/routes/index.js +4 -0
- package/dist/dashboard/routes/moments.d.ts +3 -0
- package/dist/dashboard/routes/moments.js +115 -0
- package/dist/dashboard/routes/preferences.d.ts +3 -0
- package/dist/dashboard/routes/preferences.js +52 -0
- package/dist/dashboard/routes/rules.d.ts +10 -0
- package/dist/dashboard/routes/rules.js +169 -0
- package/dist/dashboard/routes/summary.js +3 -1
- package/dist/dashboard/routes/traces.js +7 -4
- package/dist/dashboard/server.d.ts +9 -1
- package/dist/dashboard/server.js +52 -3
- package/dist/eval/citation-verify/extract.d.ts +11 -0
- package/dist/eval/citation-verify/extract.js +102 -0
- package/dist/eval/citation-verify/resolve.d.ts +26 -0
- package/dist/eval/citation-verify/resolve.js +237 -0
- package/dist/eval/citation-verify/verifier.d.ts +43 -0
- package/dist/eval/citation-verify/verifier.js +203 -0
- package/dist/eval/decision-moment.d.ts +12 -0
- package/dist/eval/decision-moment.js +181 -0
- package/dist/eval/llm-judge/client.d.ts +28 -0
- package/dist/eval/llm-judge/client.js +183 -0
- package/dist/eval/llm-judge/evaluator.d.ts +32 -0
- package/dist/eval/llm-judge/evaluator.js +138 -0
- package/dist/eval/llm-judge/pricing.d.ts +9 -0
- package/dist/eval/llm-judge/pricing.js +31 -0
- package/dist/eval/llm-judge/templates/index.d.ts +20 -0
- package/dist/eval/llm-judge/templates/index.js +170 -0
- package/dist/eval/rules/custom.js +13 -2
- package/dist/eval/rules/relevance.js +51 -6
- package/dist/eval/rules/safety.d.ts +1 -0
- package/dist/eval/rules/safety.js +87 -3
- package/dist/index.js +77 -14
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/tenant.d.ts +17 -0
- package/dist/middleware/tenant.js +26 -0
- package/dist/otel/exporter.d.ts +24 -0
- package/dist/otel/exporter.js +116 -0
- package/dist/otel/lazy.d.ts +5 -0
- package/dist/otel/lazy.js +31 -0
- package/dist/otel/mapper.d.ts +24 -0
- package/dist/otel/mapper.js +208 -0
- package/dist/preferences.d.ts +129 -0
- package/dist/preferences.js +152 -0
- package/dist/resources/dashboard-summary.js +3 -1
- package/dist/resources/trace-detail.js +5 -3
- package/dist/server.d.ts +3 -1
- package/dist/server.js +9 -3
- package/dist/storage/migrations/004-tenant-id.d.ts +3 -0
- package/dist/storage/migrations/004-tenant-id.js +40 -0
- package/dist/storage/migrations/index.js +2 -1
- package/dist/storage/sqlite-adapter.d.ts +17 -15
- package/dist/storage/sqlite-adapter.js +130 -79
- package/dist/tools/delete-rule.d.ts +3 -0
- package/dist/tools/delete-rule.js +53 -0
- package/dist/tools/delete-trace.d.ts +3 -0
- package/dist/tools/delete-trace.js +54 -0
- package/dist/tools/deploy-rule.d.ts +3 -0
- package/dist/tools/deploy-rule.js +91 -0
- package/dist/tools/evaluate-output.js +23 -2
- package/dist/tools/evaluate-with-llm-judge.d.ts +3 -0
- package/dist/tools/evaluate-with-llm-judge.js +147 -0
- package/dist/tools/get-traces.js +23 -3
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +13 -1
- package/dist/tools/list-rules.d.ts +3 -0
- package/dist/tools/list-rules.js +66 -0
- package/dist/tools/log-trace.js +30 -2
- package/dist/tools/verify-citations.d.ts +3 -0
- package/dist/tools/verify-citations.js +157 -0
- package/dist/types/custom-rule.d.ts +70 -0
- package/dist/types/custom-rule.js +1 -0
- package/dist/types/decision-moment.d.ts +122 -0
- package/dist/types/decision-moment.js +17 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -1
- package/dist/types/query.d.ts +25 -15
- package/dist/types/tenant.d.ts +26 -0
- package/dist/types/tenant.js +58 -0
- package/dist/utils/open-browser.d.ts +1 -0
- package/dist/utils/open-browser.js +45 -0
- package/dist/utils/validate-port-config.d.ts +2 -0
- package/dist/utils/validate-port-config.js +9 -0
- package/package.json +4 -1
- package/server.json +2 -2
- package/dist/dashboard/assets/index-CnDg6bYi.js +0 -43
- package/dist/dashboard/assets/index-ZsBou2-c.css +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
2
3
|
const CustomRuleSchema = z.object({
|
|
3
4
|
name: z.string(),
|
|
4
5
|
type: z.enum([
|
|
@@ -25,8 +26,26 @@ const inputSchema = {
|
|
|
25
26
|
export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
26
27
|
server.registerTool('evaluate_output', {
|
|
27
28
|
title: 'Evaluate Output',
|
|
28
|
-
description:
|
|
29
|
+
description: [
|
|
30
|
+
'Score agent output against configurable eval rules and return a 0..1 score + per-rule breakdown.',
|
|
31
|
+
'',
|
|
32
|
+
'Behavior. Deterministic, in-process scoring — same inputs always produce the same result. Writes one eval_result row to Iris storage (linked to trace_id if provided; unlinked otherwise). No external network calls in heuristic mode (v0.4 adds an llm_as_judge eval_type that DOES call LLM APIs; see the separate evaluate_with_llm_judge tool for that). Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio. Runs in ~5-50ms for rule-based evaluation.',
|
|
33
|
+
'',
|
|
34
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "score": 0..1, "passed": boolean, "rule_results": [{ "ruleName", "passed", "score", "message", "skipped?" }], "suggestions": string[], "rules_evaluated": number, "rules_skipped": number, "insufficient_data": boolean }`. `insufficient_data=true` means no applicable rules fired (e.g., safety eval with only cost data).',
|
|
35
|
+
'',
|
|
36
|
+
'Use when you want a quality score on a specific output — typically after log_trace records the execution. Pass `eval_type` to route to the right rule bundle: `completeness` (length, sentence count, relevance to input), `relevance` (keyword overlap, topic consistency), `safety` (PII leak, prompt injection, hallucination markers, stub-output detection), `cost` (budget threshold), or `custom` (bring your own rules via `custom_rules`).',
|
|
37
|
+
'',
|
|
38
|
+
'Don\'t use when the output is empty or has no applicable rules — the eval_type decides which rules apply, and invalid combinations return score=0 + insufficient_data=true (not an error, but not actionable). Don\'t use to VALIDATE JSON schemas directly (use your language\'s JSON Schema validator — Iris\'s `json_schema` custom rule type is for output-shape assertions, not arbitrary validation).',
|
|
39
|
+
'',
|
|
40
|
+
'Error modes. Throws on malformed custom_rules (Zod rejects). Returns 400 on regex patterns that fail safe-regex2 ReDoS check or exceed 1000-char limit. Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. The eval itself never throws — failing rules report `passed: false` with a message, they don\'t bubble exceptions.',
|
|
41
|
+
].join('\n'),
|
|
29
42
|
inputSchema,
|
|
43
|
+
annotations: {
|
|
44
|
+
readOnlyHint: false, // Writes an eval_result row
|
|
45
|
+
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
46
|
+
idempotentHint: true, // Deterministic: same inputs → same score (each call writes a distinct result row, but the SCORE is stable)
|
|
47
|
+
openWorldHint: false, // No external network in heuristic mode; LLM-as-judge has its own tool with openWorldHint:true
|
|
48
|
+
},
|
|
30
49
|
}, async (args) => {
|
|
31
50
|
const evalType = args.eval_type;
|
|
32
51
|
const result = evalEngine.evaluate(evalType, {
|
|
@@ -39,7 +58,9 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
39
58
|
if (args.trace_id) {
|
|
40
59
|
result.trace_id = args.trace_id;
|
|
41
60
|
}
|
|
42
|
-
|
|
61
|
+
// OSS single-tenant: MCP tool callers are the local user. Cloud
|
|
62
|
+
// will derive tenant from the authenticated MCP session.
|
|
63
|
+
await storage.insertEvalResult(LOCAL_TENANT, result);
|
|
43
64
|
return {
|
|
44
65
|
content: [
|
|
45
66
|
{
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { evaluateWithLLMJudge } from '../eval/llm-judge/evaluator.js';
|
|
4
|
+
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
|
+
import { generateEvalId } from '../utils/ids.js';
|
|
6
|
+
const inputSchema = {
|
|
7
|
+
output: z.string().min(1).describe('The agent output text to evaluate'),
|
|
8
|
+
template: z
|
|
9
|
+
.enum(['accuracy', 'helpfulness', 'safety', 'correctness', 'faithfulness'])
|
|
10
|
+
.describe('Judge dimension: accuracy (factual correctness), helpfulness (does it address the ask), safety (harm potential), correctness (vs reference answer — requires `expected`), faithfulness (RAG grounding — requires `source_material`).'),
|
|
11
|
+
model: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('Model ID. 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.'),
|
|
14
|
+
provider: z.enum(['anthropic', 'openai']).optional().describe('Auto-detected from model when omitted'),
|
|
15
|
+
input: z.string().optional().describe('User question / prompt that produced the output (improves accuracy for helpfulness/safety)'),
|
|
16
|
+
expected: z.string().optional().describe('Reference answer (required for correctness template)'),
|
|
17
|
+
source_material: z.string().optional().describe('Provided RAG sources (required for faithfulness template)'),
|
|
18
|
+
trace_id: z.string().optional().describe('Link this evaluation to a trace'),
|
|
19
|
+
max_cost_usd: z.number().positive().optional().describe('Cost cap in USD; defaults to IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or 0.25'),
|
|
20
|
+
max_output_tokens: z.number().int().positive().max(4096).optional().describe('Judge output token cap; default 512'),
|
|
21
|
+
temperature: z.number().min(0).max(2).optional().describe('Sampling temperature; default 0 (deterministic)'),
|
|
22
|
+
timeout_ms: z.number().int().positive().optional().describe('Per-request timeout; default 60_000'),
|
|
23
|
+
};
|
|
24
|
+
function inferProvider(model) {
|
|
25
|
+
const pricing = findPricing(model);
|
|
26
|
+
if (!pricing) {
|
|
27
|
+
throw new Error(`Unknown model "${model}". Provider cannot be inferred. Supported models are listed in src/eval/llm-judge/pricing.ts.`);
|
|
28
|
+
}
|
|
29
|
+
return pricing.provider;
|
|
30
|
+
}
|
|
31
|
+
function resolveApiKey(provider) {
|
|
32
|
+
if (provider === 'anthropic') {
|
|
33
|
+
const key = process.env.IRIS_ANTHROPIC_API_KEY;
|
|
34
|
+
if (!key) {
|
|
35
|
+
throw new Error('Anthropic judge requires IRIS_ANTHROPIC_API_KEY. Set it in the environment or use a different provider.');
|
|
36
|
+
}
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
const key = process.env.IRIS_OPENAI_API_KEY;
|
|
40
|
+
if (!key) {
|
|
41
|
+
throw new Error('OpenAI judge requires IRIS_OPENAI_API_KEY. Set it in the environment or use a different provider.');
|
|
42
|
+
}
|
|
43
|
+
return key;
|
|
44
|
+
}
|
|
45
|
+
function resolveMaxCost(paramValue) {
|
|
46
|
+
if (paramValue !== undefined)
|
|
47
|
+
return paramValue;
|
|
48
|
+
const envRaw = process.env.IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL;
|
|
49
|
+
if (envRaw) {
|
|
50
|
+
const parsed = Number(envRaw);
|
|
51
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
return 0.25;
|
|
55
|
+
}
|
|
56
|
+
export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
57
|
+
server.registerTool('evaluate_with_llm_judge', {
|
|
58
|
+
title: 'Evaluate With LLM Judge',
|
|
59
|
+
description: [
|
|
60
|
+
'Score agent output using an LLM as the judge (Anthropic or OpenAI). Returns a calibrated 0..1 score with rationale, per-dimension breakdown, and exact cost.',
|
|
61
|
+
'',
|
|
62
|
+
'Behavior. Calls an external LLM API (Anthropic or OpenAI) — costs money per call, takes 1-10 seconds, respects an IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL cap. Non-deterministic at temperature > 0; default temperature=0 gives near-deterministic scores. Writes one eval_result row to Iris storage (linked to trace_id if provided) plus captures provider response id + latency + token counts + cost in the rule_results payload. Rate-limited to 20 req/min on HTTP MCP; your LLM provider also enforces its own rate limits (we transparently retry once on 429).',
|
|
63
|
+
'',
|
|
64
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "score": 0..1, "passed": boolean, "rationale": string, "dimensions": {...}, "model": string, "provider": "anthropic"|"openai", "template": string, "input_tokens": number, "output_tokens": number, "cost_usd": number, "latency_ms": number }`. `dimensions` has per-dimension sub-scores (e.g., accuracy template returns `{factual_claims, citations, internal_consistency}`).',
|
|
65
|
+
'',
|
|
66
|
+
'Use when heuristic rules (via evaluate_output) are too coarse for the quality signal you need — semantic correctness, factual accuracy vs a reference, RAG faithfulness to sources, nuanced safety/helpfulness. Pick the template that matches: `accuracy` (hallucination detection), `helpfulness` (does it address the ask), `safety` (harm potential beyond regex PII), `correctness` (vs reference answer — pass `expected`), `faithfulness` (RAG grounding — pass `source_material`).',
|
|
67
|
+
'',
|
|
68
|
+
"Don't use for simple regex/length/keyword checks (use evaluate_output with heuristic rules — they're free, deterministic, 1000x faster). Don't use without an API key set (IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY). Don't use on very large outputs (>8K tokens) without raising max_cost_usd — the pre-check will refuse the call.",
|
|
69
|
+
'',
|
|
70
|
+
'Error modes. Throws when the required API key env var is missing. Throws when the estimated worst-case cost exceeds max_cost_usd (raise the cap or trim prompts). Throws LLMJudgeError on provider errors — kind=`auth` on 401/403, `rate_limit` on 429 (auto-retried once), `server_error` on 5xx, `timeout` on abort, `malformed_response` when the judge fails to emit valid JSON on both attempts. Throws "Unknown model" for unsupported model IDs — update src/eval/llm-judge/pricing.ts first.',
|
|
71
|
+
].join('\n'),
|
|
72
|
+
inputSchema,
|
|
73
|
+
annotations: {
|
|
74
|
+
readOnlyHint: false, // Writes eval_result; also spends money (external API cost)
|
|
75
|
+
destructiveHint: false, // Creates data; doesn't overwrite or delete
|
|
76
|
+
idempotentHint: false, // Temperature > 0 may vary; even at T=0 provider non-determinism is possible; cost also varies per call
|
|
77
|
+
openWorldHint: true, // Calls external APIs (Anthropic / OpenAI) — touches the world beyond local process
|
|
78
|
+
},
|
|
79
|
+
}, async (args) => {
|
|
80
|
+
const provider = args.provider ?? inferProvider(args.model);
|
|
81
|
+
const apiKey = resolveApiKey(provider);
|
|
82
|
+
const maxCostUsd = resolveMaxCost(args.max_cost_usd);
|
|
83
|
+
const result = await evaluateWithLLMJudge({
|
|
84
|
+
output: args.output,
|
|
85
|
+
template: args.template,
|
|
86
|
+
provider,
|
|
87
|
+
model: args.model,
|
|
88
|
+
apiKey,
|
|
89
|
+
input: args.input,
|
|
90
|
+
expected: args.expected,
|
|
91
|
+
sourceMaterial: args.source_material,
|
|
92
|
+
maxCostUsdPerEval: maxCostUsd,
|
|
93
|
+
maxOutputTokens: args.max_output_tokens,
|
|
94
|
+
temperature: args.temperature,
|
|
95
|
+
timeoutMs: args.timeout_ms,
|
|
96
|
+
});
|
|
97
|
+
const evalId = generateEvalId();
|
|
98
|
+
// Persist as a normal eval_result so the dashboard picks it up
|
|
99
|
+
// alongside heuristic scores. eval_type is 'custom' because LLM
|
|
100
|
+
// judge doesn't fit completeness/relevance/safety/cost taxonomy
|
|
101
|
+
// cleanly — it spans all four. The rule_results payload carries
|
|
102
|
+
// the full judge provenance.
|
|
103
|
+
await storage.insertEvalResult(LOCAL_TENANT, {
|
|
104
|
+
id: evalId,
|
|
105
|
+
trace_id: args.trace_id,
|
|
106
|
+
eval_type: 'custom',
|
|
107
|
+
output_text: args.output,
|
|
108
|
+
expected_text: args.expected,
|
|
109
|
+
score: result.score,
|
|
110
|
+
passed: result.passed,
|
|
111
|
+
rule_results: [
|
|
112
|
+
{
|
|
113
|
+
ruleName: `llm_judge:${result.template}:${result.provider}/${result.model}`,
|
|
114
|
+
passed: result.passed,
|
|
115
|
+
score: result.score,
|
|
116
|
+
message: result.rationale || 'LLM judge evaluation',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
suggestions: result.passed ? [] : [result.rationale],
|
|
120
|
+
rules_evaluated: 1,
|
|
121
|
+
rules_skipped: 0,
|
|
122
|
+
insufficient_data: false,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: JSON.stringify({
|
|
129
|
+
id: evalId,
|
|
130
|
+
score: result.score,
|
|
131
|
+
passed: result.passed,
|
|
132
|
+
rationale: result.rationale,
|
|
133
|
+
dimensions: result.dimensions,
|
|
134
|
+
model: result.model,
|
|
135
|
+
provider: result.provider,
|
|
136
|
+
template: result.template,
|
|
137
|
+
input_tokens: result.inputTokens,
|
|
138
|
+
output_tokens: result.outputTokens,
|
|
139
|
+
cost_usd: result.costUsd,
|
|
140
|
+
latency_ms: result.latencyMs,
|
|
141
|
+
raw_response_id: result.rawResponseId,
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
package/dist/tools/get-traces.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
2
3
|
const inputSchema = {
|
|
3
4
|
agent_name: z.string().optional().describe('Filter by agent name'),
|
|
4
5
|
framework: z.string().optional().describe('Filter by framework'),
|
|
@@ -15,10 +16,29 @@ const inputSchema = {
|
|
|
15
16
|
export function registerGetTracesTool(server, storage) {
|
|
16
17
|
server.registerTool('get_traces', {
|
|
17
18
|
title: 'Get Traces',
|
|
18
|
-
description:
|
|
19
|
+
description: [
|
|
20
|
+
'Query stored agent-execution traces with filters, pagination, and optional dashboard summary.',
|
|
21
|
+
'',
|
|
22
|
+
'Behavior. Read-only: never mutates storage, never calls external services. Idempotent: repeated calls with the same args return consistent results (new traces logged after the call obviously show up on subsequent calls). Tenant-scoped: queries only the caller\'s tenant rows (LOCAL_TENANT in OSS). Paginates results (default limit 50, max 1000). Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio.',
|
|
23
|
+
'',
|
|
24
|
+
'Output shape. Returns JSON: `{ "traces": [{...traceRow}], "total": number, "limit": number, "offset": number, "summary"?: { total_traces, avg_latency_ms, total_cost_usd, error_rate, eval_pass_rate, traces_per_hour, top_agents } }`. Each trace row includes trace_id, agent_name, framework, input, output, tool_calls, latency_ms, token_usage, cost_usd, metadata, timestamp. `summary` only included when `include_summary: true`.',
|
|
25
|
+
'',
|
|
26
|
+
'Use when you need historical data: investigating a past failure, computing quality trends, comparing agents, or feeding an analytics job. Set `agent_name` / `framework` / `since` / `until` to narrow the query. Set `min_score` / `max_score` to surface outliers. Set `sort_by: "cost_usd"` + `sort_order: "desc"` to find the most expensive traces. Set `include_summary: true` when you want dashboard-style aggregates in one round-trip.',
|
|
27
|
+
'',
|
|
28
|
+
'Don\'t use to score a trace (use evaluate_output). Don\'t use to create a trace (use log_trace). Don\'t use as a live event stream — it\'s a query, not a subscription; poll with exponential backoff or use the dashboard\'s SSE endpoint for real-time.',
|
|
29
|
+
'',
|
|
30
|
+
'Error modes. Returns 400 on invalid sort_by / sort_order (Zod enum). Returns 400 if limit > 1000. Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. Empty result with `total: 0` on no matches (not an error).',
|
|
31
|
+
].join('\n'),
|
|
19
32
|
inputSchema,
|
|
33
|
+
annotations: {
|
|
34
|
+
readOnlyHint: true, // Pure query: never writes, never deletes
|
|
35
|
+
destructiveHint: false, // Inverse of readOnly — trivially false
|
|
36
|
+
idempotentHint: true, // Same args → same result (modulo new traces that may have landed since)
|
|
37
|
+
openWorldHint: false, // Queries local storage only; no external network
|
|
38
|
+
},
|
|
20
39
|
}, async (args) => {
|
|
21
|
-
|
|
40
|
+
// OSS single-tenant: MCP caller is the local user.
|
|
41
|
+
const result = await storage.queryTraces(LOCAL_TENANT, {
|
|
22
42
|
filter: {
|
|
23
43
|
agent_name: args.agent_name,
|
|
24
44
|
framework: args.framework,
|
|
@@ -39,7 +59,7 @@ export function registerGetTracesTool(server, storage) {
|
|
|
39
59
|
offset: result.offset,
|
|
40
60
|
};
|
|
41
61
|
if (args.include_summary) {
|
|
42
|
-
response.summary = await storage.getDashboardSummary();
|
|
62
|
+
response.summary = await storage.getDashboardSummary(LOCAL_TENANT);
|
|
43
63
|
}
|
|
44
64
|
return {
|
|
45
65
|
content: [
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { IStorageAdapter } from '../types/query.js';
|
|
3
3
|
import type { EvalEngine } from '../eval/engine.js';
|
|
4
|
-
|
|
4
|
+
import type { CustomRuleStore } from '../custom-rule-store.js';
|
|
5
|
+
export declare function registerAllTools(server: McpServer, storage: IStorageAdapter, evalEngine: EvalEngine, customRuleStore: CustomRuleStore): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { registerLogTraceTool } from './log-trace.js';
|
|
2
2
|
import { registerEvaluateOutputTool } from './evaluate-output.js';
|
|
3
3
|
import { registerGetTracesTool } from './get-traces.js';
|
|
4
|
-
|
|
4
|
+
import { registerListRulesTool } from './list-rules.js';
|
|
5
|
+
import { registerDeployRuleTool } from './deploy-rule.js';
|
|
6
|
+
import { registerDeleteRuleTool } from './delete-rule.js';
|
|
7
|
+
import { registerDeleteTraceTool } from './delete-trace.js';
|
|
8
|
+
import { registerEvaluateWithLLMJudgeTool } from './evaluate-with-llm-judge.js';
|
|
9
|
+
import { registerVerifyCitationsTool } from './verify-citations.js';
|
|
10
|
+
export function registerAllTools(server, storage, evalEngine, customRuleStore) {
|
|
5
11
|
registerLogTraceTool(server, storage);
|
|
6
12
|
registerEvaluateOutputTool(server, storage, evalEngine);
|
|
7
13
|
registerGetTracesTool(server, storage);
|
|
14
|
+
registerListRulesTool(server, customRuleStore);
|
|
15
|
+
registerDeployRuleTool(server, customRuleStore);
|
|
16
|
+
registerDeleteRuleTool(server, customRuleStore);
|
|
17
|
+
registerDeleteTraceTool(server, storage);
|
|
18
|
+
registerEvaluateWithLLMJudgeTool(server, storage);
|
|
19
|
+
registerVerifyCitationsTool(server, storage);
|
|
8
20
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* list_rules MCP tool — enumerate deployed custom rules.
|
|
3
|
+
*
|
|
4
|
+
* Read-only view into the custom-rule store (~/.iris/custom-rules.json).
|
|
5
|
+
* Lets agents discover what rules are deployed, what each one evaluates,
|
|
6
|
+
* and which are enabled — so an agent can decide whether to call
|
|
7
|
+
* evaluate_output at all, and which eval_type to route through.
|
|
8
|
+
*
|
|
9
|
+
* Companion to deploy_rule / delete_rule. Together these replace the
|
|
10
|
+
* dashboard-only Make-This-A-Rule composer when an agent (not a human)
|
|
11
|
+
* needs to manage the rule set programmatically.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
const inputSchema = {
|
|
15
|
+
eval_type: z
|
|
16
|
+
.enum(['completeness', 'relevance', 'safety', 'cost', 'custom'])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Filter to rules of a specific eval category'),
|
|
19
|
+
enabled_only: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe('Return only enabled rules (excludes disabled ones)'),
|
|
23
|
+
};
|
|
24
|
+
export function registerListRulesTool(server, customRuleStore) {
|
|
25
|
+
server.registerTool('list_rules', {
|
|
26
|
+
title: 'List Custom Rules',
|
|
27
|
+
description: [
|
|
28
|
+
'Enumerate deployed custom evaluation rules from the local rule store.',
|
|
29
|
+
'',
|
|
30
|
+
'Behavior. Pure read of ~/.iris/custom-rules.json (in-memory cached; no disk read per call after server boot). No mutation, no external network. Tenant-scoped in Cloud tier; OSS returns all rules for the single local tenant. Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio. Returns in <5ms.',
|
|
31
|
+
'',
|
|
32
|
+
'Output shape. Returns JSON: `{ "rules": [{ "id": "rule-XXXX", "name", "description?", "evalType", "severity", "definition": { type, config, weight? }, "enabled": boolean, "deployedAt": ISO timestamp, "sourceMomentId?": string }], "total": number, "enabled_count": number }`. Empty array + total=0 when no rules deployed.',
|
|
33
|
+
'',
|
|
34
|
+
'Use when you need to know what custom rules are currently live (before calling evaluate_output, before deploying a similar rule to avoid duplicates, or when building a dashboard view). Filter with `eval_type` to scope to a specific category, or `enabled_only: true` to exclude disabled rules. Use get_traces to see trace data; use evaluate_output to run scoring; use list_rules only when you need the RULE INVENTORY.',
|
|
35
|
+
'',
|
|
36
|
+
"Don't use to count traces or evals (that's get_traces). Don't use to inspect built-in (non-custom) rules — those ship with the iris binary and are listed in docs/api-reference.md, not in the rule store. Don't use to deploy a rule (use deploy_rule); don't use to remove one (use delete_rule).",
|
|
37
|
+
'',
|
|
38
|
+
"Error modes. Returns empty list if the rule store file doesn't exist (first run). Returns 429 if HTTP rate limit exceeded. Never throws on valid input.",
|
|
39
|
+
].join('\n'),
|
|
40
|
+
inputSchema,
|
|
41
|
+
annotations: {
|
|
42
|
+
readOnlyHint: true,
|
|
43
|
+
destructiveHint: false,
|
|
44
|
+
idempotentHint: true,
|
|
45
|
+
openWorldHint: false,
|
|
46
|
+
},
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
let rules = customRuleStore.list();
|
|
49
|
+
if (args.eval_type) {
|
|
50
|
+
rules = rules.filter((r) => r.evalType === args.eval_type);
|
|
51
|
+
}
|
|
52
|
+
if (args.enabled_only) {
|
|
53
|
+
rules = rules.filter((r) => r.enabled);
|
|
54
|
+
}
|
|
55
|
+
const total = rules.length;
|
|
56
|
+
const enabled_count = rules.filter((r) => r.enabled).length;
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: JSON.stringify({ rules, total, enabled_count }),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/tools/log-trace.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { generateTraceId, generateSpanId } from '../utils/ids.js';
|
|
3
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
4
|
+
import { bestEffortExport } from '../otel/lazy.js';
|
|
3
5
|
const ToolCallSchema = z.object({
|
|
4
6
|
tool_name: z.string(),
|
|
5
7
|
input: z.unknown().optional(),
|
|
@@ -44,8 +46,26 @@ const inputSchema = {
|
|
|
44
46
|
export function registerLogTraceTool(server, storage) {
|
|
45
47
|
server.registerTool('log_trace', {
|
|
46
48
|
title: 'Log Trace',
|
|
47
|
-
description:
|
|
49
|
+
description: [
|
|
50
|
+
'Persist a single agent execution trace (input, output, spans, tool calls, cost, latency, token usage).',
|
|
51
|
+
'',
|
|
52
|
+
'Behavior. Writes one row to Iris storage (SQLite by default; Postgres in Cloud tier). When IRIS_OTEL_ENDPOINT is set, ALSO fires a best-effort async export to the configured OTLP/HTTP collector (Jaeger, Tempo, Datadog OTLP, OTEL Collector). The OTel export is fire-and-forget — its success does not affect the tool response; failures are logged but the trace is still stored locally. No authentication in stdio mode; HTTP mode requires Bearer token. Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio. Not idempotent: each call mints a fresh trace_id, so resubmitting the same payload creates a duplicate trace.',
|
|
53
|
+
'',
|
|
54
|
+
'Output shape. Returns a JSON string: `{ "trace_id": "<32-hex>", "status": "stored" }`. The trace_id is the key you pass to evaluate_output or get_traces afterwards.',
|
|
55
|
+
'',
|
|
56
|
+
'Use when you want to record an agent execution for later evaluation, analysis, or audit. Call it AFTER the agent has produced output; call evaluate_output afterwards to score it; call get_traces to query historical traces. Store rich context: spans (span tree), tool_calls (which tools were invoked with latency/errors), token_usage, cost_usd, metadata (arbitrary key-value). All optional except agent_name.',
|
|
57
|
+
'',
|
|
58
|
+
'Don\'t use when you only need a transient log (use console logging). Don\'t use to update an existing trace — there is no update path in v0.4 (traces are immutable once stored).',
|
|
59
|
+
'',
|
|
60
|
+
'Error modes. Throws on missing agent_name. Throws on malformed span or tool_call objects (Zod rejects). Returns 500 on storage failure (disk full, DB locked). Never blocks on the agent — returns within ~50ms for typical payloads.',
|
|
61
|
+
].join('\n'),
|
|
48
62
|
inputSchema,
|
|
63
|
+
annotations: {
|
|
64
|
+
readOnlyHint: false, // Writes a row to storage
|
|
65
|
+
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
66
|
+
idempotentHint: false, // Each call mints a fresh trace_id; duplicate payloads produce distinct traces
|
|
67
|
+
openWorldHint: false, // Local storage first. When IRIS_OTEL_ENDPOINT is set a best-effort async OTel export runs but is non-blocking (tool succeeds even if export fails).
|
|
68
|
+
},
|
|
49
69
|
}, async (args) => {
|
|
50
70
|
const traceId = generateTraceId();
|
|
51
71
|
const timestamp = args.timestamp ?? new Date().toISOString();
|
|
@@ -67,7 +87,15 @@ export function registerLogTraceTool(server, storage) {
|
|
|
67
87
|
trace_id: traceId,
|
|
68
88
|
})),
|
|
69
89
|
};
|
|
70
|
-
await storage.insertTrace(trace);
|
|
90
|
+
await storage.insertTrace(LOCAL_TENANT, trace);
|
|
91
|
+
// Best-effort async OTel export (fire-and-forget). No-op when
|
|
92
|
+
// IRIS_OTEL_ENDPOINT isn't configured. Errors are logged via the
|
|
93
|
+
// server logger but never affect the tool response — if the OTel
|
|
94
|
+
// collector is down we still want to store traces locally.
|
|
95
|
+
bestEffortExport(trace, (err) => {
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.warn(`[iris.otel] ${err.message}`);
|
|
98
|
+
});
|
|
71
99
|
return {
|
|
72
100
|
content: [
|
|
73
101
|
{
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { verifyCitations } from '../eval/citation-verify/verifier.js';
|
|
4
|
+
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
|
+
import { generateEvalId } from '../utils/ids.js';
|
|
6
|
+
const inputSchema = {
|
|
7
|
+
output: z.string().min(1).describe('The agent output containing citations to verify'),
|
|
8
|
+
model: z
|
|
9
|
+
.string()
|
|
10
|
+
.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.'),
|
|
11
|
+
provider: z.enum(['anthropic', 'openai']).optional().describe('Auto-detected from model when omitted'),
|
|
12
|
+
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.'),
|
|
13
|
+
domain_allowlist: z
|
|
14
|
+
.array(z.string())
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Restrict fetches to hostnames in this list (suffix match allowed). Merged with IRIS_CITATION_DOMAINS env.'),
|
|
17
|
+
max_cost_usd_total: z.number().positive().optional().describe('Cap TOTAL judge cost across all citations in this call; default $1.00'),
|
|
18
|
+
max_citations: z.number().int().positive().max(50).optional().describe('Max citations to verify (extras skipped); default 20'),
|
|
19
|
+
per_source_timeout_ms: z.number().int().positive().optional().describe('Per-URL fetch timeout; default 10_000'),
|
|
20
|
+
per_source_max_bytes: z.number().int().positive().optional().describe('Per-URL body cap; default 5MB'),
|
|
21
|
+
trace_id: z.string().optional().describe('Link verification result to a trace'),
|
|
22
|
+
};
|
|
23
|
+
function inferProvider(model) {
|
|
24
|
+
const pricing = findPricing(model);
|
|
25
|
+
if (!pricing) {
|
|
26
|
+
throw new Error(`Unknown model "${model}". Provider cannot be inferred. Supported models: src/eval/llm-judge/pricing.ts.`);
|
|
27
|
+
}
|
|
28
|
+
return pricing.provider;
|
|
29
|
+
}
|
|
30
|
+
function resolveApiKey(provider) {
|
|
31
|
+
const key = provider === 'anthropic' ? process.env.IRIS_ANTHROPIC_API_KEY : process.env.IRIS_OPENAI_API_KEY;
|
|
32
|
+
if (!key) {
|
|
33
|
+
throw new Error(`${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} judge requires IRIS_${provider === 'anthropic' ? 'ANTHROPIC' : 'OPENAI'}_API_KEY for verify_citations.`);
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
function resolveAllowFetch(paramValue) {
|
|
38
|
+
if (paramValue !== undefined)
|
|
39
|
+
return paramValue;
|
|
40
|
+
return process.env.IRIS_CITATION_ALLOW_FETCH === '1';
|
|
41
|
+
}
|
|
42
|
+
function resolveDomainAllowlist(paramValue) {
|
|
43
|
+
const envRaw = process.env.IRIS_CITATION_DOMAINS;
|
|
44
|
+
const fromEnv = envRaw ? envRaw.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
45
|
+
if (paramValue && paramValue.length > 0) {
|
|
46
|
+
return [...new Set([...fromEnv, ...paramValue])];
|
|
47
|
+
}
|
|
48
|
+
return fromEnv.length > 0 ? fromEnv : undefined;
|
|
49
|
+
}
|
|
50
|
+
export function registerVerifyCitationsTool(server, storage) {
|
|
51
|
+
server.registerTool('verify_citations', {
|
|
52
|
+
title: 'Verify Citations',
|
|
53
|
+
description: [
|
|
54
|
+
'Extract citations from agent output, fetch the cited sources, and use an LLM judge to check whether each source supports the claim in context. Returns per-citation verdicts + an overall support ratio.',
|
|
55
|
+
'',
|
|
56
|
+
'Behavior. Three-phase pipeline: (1) regex extraction of [N] numbered refs, (Author, Year) parentheticals, bare URLs, and DOIs (in-process, no network); (2) SSRF-guarded fetch of URL + DOI citations, with scheme allowlist, private/link-local/cloud-metadata IP blocking, optional domain allowlist (IRIS_CITATION_DOMAINS), 10s timeout, 5MB body cap, manual redirect chase (max 3, re-checked), in-process LRU cache; (3) per-citation LLM judge call asking "does this source support this claim?" with a 256-token verdict. Opt-in via allow_fetch=true or IRIS_CITATION_ALLOW_FETCH=1 — Iris refuses outbound HTTP by default. Cost-capped across the entire call by max_cost_usd_total (default $1.00) — the pipeline stops when the cap would be exceeded. Rate-limited to 20 req/min on HTTP MCP. Writes one eval_result row tagged with per-citation provenance.',
|
|
57
|
+
'',
|
|
58
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "overall_score": 0..1|null, "passed": boolean, "total_citations_found": number, "total_resolved": number, "total_supported": number, "total_cost_usd": number, "citations": [{ "citation": { "raw", "kind", "identifier", "offset_start", "offset_end" }, "resolve_status": "ok"|"skipped"|"error", "resolve_error"?, "source"?: { "url", "status", "content_type", "bytes_fetched", "truncated" }, "judge"?: { "supported", "confidence", "rationale", "cost_usd", "latency_ms", "input_tokens", "output_tokens" } }] }`. `overall_score = supported / resolved`; `null` when nothing resolvable was found.',
|
|
59
|
+
'',
|
|
60
|
+
'Use when the output makes factual claims backed by [1]-style references, DOIs, or URLs and you want to separate "cited correctly" from "cited and wrong" from "cited but unresolvable". Particularly useful for research/legal/medical agents where fabricated citations are the dominant failure mode.',
|
|
61
|
+
"",
|
|
62
|
+
"Don't use when the agent output has no citations at all (overall_score will be null; the tool degrades gracefully but a heuristic rule is cheaper). Don't use without allow_fetch=true or IRIS_CITATION_ALLOW_FETCH=1 — the tool refuses outbound HTTP unless explicitly enabled. Don't use with an open allowlist + untrusted output on the public internet; you are effectively running a user-directed fetcher. For stricter safety set IRIS_CITATION_DOMAINS to a curated list.",
|
|
63
|
+
'',
|
|
64
|
+
'Error modes. Throws when the API key env var is missing. Throws "Unknown model" on unsupported model IDs. Per-citation errors are collected (resolve_error.kind = bad_scheme / ssrf / not_allowed_domain / timeout / too_large / bad_status / redirect_loop / not_text / fetch_disabled / malformed_judge_response / cost_cap_reached / unresolvable_kind) and returned in the response rather than thrown. An empty output or output with zero extractable citations returns overall_score=null + passed=true (nothing to fail).',
|
|
65
|
+
].join('\n'),
|
|
66
|
+
inputSchema,
|
|
67
|
+
annotations: {
|
|
68
|
+
readOnlyHint: false, // Writes eval_result + spends money
|
|
69
|
+
destructiveHint: false, // Creates data; doesn't overwrite/delete
|
|
70
|
+
idempotentHint: false, // External fetches + provider non-determinism
|
|
71
|
+
openWorldHint: true, // Outbound HTTP to citation URLs + LLM provider API
|
|
72
|
+
},
|
|
73
|
+
}, async (args) => {
|
|
74
|
+
const provider = args.provider ?? inferProvider(args.model);
|
|
75
|
+
const apiKey = resolveApiKey(provider);
|
|
76
|
+
const allowFetch = resolveAllowFetch(args.allow_fetch);
|
|
77
|
+
const domainAllowlist = resolveDomainAllowlist(args.domain_allowlist);
|
|
78
|
+
const result = await verifyCitations({
|
|
79
|
+
output: args.output,
|
|
80
|
+
provider,
|
|
81
|
+
model: args.model,
|
|
82
|
+
apiKey,
|
|
83
|
+
allowFetch,
|
|
84
|
+
domainAllowlist,
|
|
85
|
+
maxCostUsdTotal: args.max_cost_usd_total,
|
|
86
|
+
maxCitations: args.max_citations,
|
|
87
|
+
perSourceTimeoutMs: args.per_source_timeout_ms,
|
|
88
|
+
perSourceMaxBytes: args.per_source_max_bytes,
|
|
89
|
+
});
|
|
90
|
+
const evalId = generateEvalId();
|
|
91
|
+
const score = result.overallScore ?? 0;
|
|
92
|
+
// Persist so dashboard can surface. eval_type='custom' — same
|
|
93
|
+
// rationale as evaluate_with_llm_judge (spans all 4 heuristic
|
|
94
|
+
// categories). rule_results[0] carries per-citation summary.
|
|
95
|
+
await storage.insertEvalResult(LOCAL_TENANT, {
|
|
96
|
+
id: evalId,
|
|
97
|
+
trace_id: args.trace_id,
|
|
98
|
+
eval_type: 'custom',
|
|
99
|
+
output_text: args.output,
|
|
100
|
+
score,
|
|
101
|
+
passed: result.passed,
|
|
102
|
+
rule_results: [
|
|
103
|
+
{
|
|
104
|
+
ruleName: `semantic_citation_verify:${provider}/${args.model}`,
|
|
105
|
+
passed: result.passed,
|
|
106
|
+
score,
|
|
107
|
+
message: result.overallScore === null
|
|
108
|
+
? `No resolvable citations (found ${result.totalCitationsFound}, resolved ${result.totalResolved})`
|
|
109
|
+
: `${result.totalSupported}/${result.totalResolved} cited sources supported the output`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
suggestions: result.passed ? [] : [`Only ${result.totalSupported}/${result.totalResolved} cited sources actually supported the claim.`],
|
|
113
|
+
rules_evaluated: 1,
|
|
114
|
+
rules_skipped: 0,
|
|
115
|
+
insufficient_data: result.overallScore === null,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: JSON.stringify({
|
|
122
|
+
id: evalId,
|
|
123
|
+
overall_score: result.overallScore,
|
|
124
|
+
passed: result.passed,
|
|
125
|
+
total_citations_found: result.totalCitationsFound,
|
|
126
|
+
total_resolved: result.totalResolved,
|
|
127
|
+
total_supported: result.totalSupported,
|
|
128
|
+
total_cost_usd: result.totalCostUsd,
|
|
129
|
+
citations: result.citations.map((c) => ({
|
|
130
|
+
citation: {
|
|
131
|
+
raw: c.citation.raw,
|
|
132
|
+
kind: c.citation.kind,
|
|
133
|
+
identifier: c.citation.identifier,
|
|
134
|
+
offset_start: c.citation.offsetStart,
|
|
135
|
+
offset_end: c.citation.offsetEnd,
|
|
136
|
+
},
|
|
137
|
+
resolve_status: c.resolveStatus,
|
|
138
|
+
resolve_error: c.resolveError,
|
|
139
|
+
source: c.source,
|
|
140
|
+
judge: c.judge
|
|
141
|
+
? {
|
|
142
|
+
supported: c.judge.supported,
|
|
143
|
+
confidence: c.judge.confidence,
|
|
144
|
+
rationale: c.judge.rationale,
|
|
145
|
+
cost_usd: c.judge.costUsd,
|
|
146
|
+
latency_ms: c.judge.latencyMs,
|
|
147
|
+
input_tokens: c.judge.inputTokens,
|
|
148
|
+
output_tokens: c.judge.outputTokens,
|
|
149
|
+
}
|
|
150
|
+
: undefined,
|
|
151
|
+
})),
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CustomRuleDefinition, EvalType } from './eval.js';
|
|
2
|
+
export type RuleSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
3
|
+
export interface DeployedCustomRule {
|
|
4
|
+
/** Stable id (e.g., "rule-<8-char-hex>"). Generated server-side on deploy. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** User-readable name. Becomes the rule name in eval results. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Human-readable description (what the rule checks for, why it matters). */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Eval category this rule belongs to. Determines when it fires. */
|
|
11
|
+
evalType: EvalType;
|
|
12
|
+
/** Severity used to sort rules in the dashboard + audit alerts. */
|
|
13
|
+
severity: RuleSeverity;
|
|
14
|
+
/** The check definition (regex pattern, length threshold, etc.). */
|
|
15
|
+
definition: CustomRuleDefinition;
|
|
16
|
+
/** Whether this rule is currently active. Disabled rules don't fire but are kept for audit. */
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** ISO timestamp of deploy. */
|
|
19
|
+
createdAt: string;
|
|
20
|
+
/** ISO timestamp of most recent edit. */
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
/** Optional moment ID the rule was extracted from (workflow inversion provenance). */
|
|
23
|
+
sourceMomentId?: string;
|
|
24
|
+
/** Version counter — incremented on edit. Starts at 1. */
|
|
25
|
+
version: number;
|
|
26
|
+
}
|
|
27
|
+
export interface CustomRulesFile {
|
|
28
|
+
/** File schema version — bump when shape changes. */
|
|
29
|
+
version: 1;
|
|
30
|
+
rules: DeployedCustomRule[];
|
|
31
|
+
}
|
|
32
|
+
export interface AuditLogEntry {
|
|
33
|
+
ts: string;
|
|
34
|
+
/**
|
|
35
|
+
* Which tenant the action belongs to. OSS installs always emit 'local'.
|
|
36
|
+
* Cloud installs emit the tenant resolved from the authenticated session.
|
|
37
|
+
*
|
|
38
|
+
* Optional for backward compatibility: entries written before v0.4.0
|
|
39
|
+
* don't have this field. Readers MUST treat missing `tenantId` as
|
|
40
|
+
* 'local' so old audit logs remain queryable on upgrade.
|
|
41
|
+
*/
|
|
42
|
+
tenantId?: string;
|
|
43
|
+
/** Action taken — currently rule.deploy / rule.delete / rule.toggle. */
|
|
44
|
+
action: 'rule.deploy' | 'rule.delete' | 'rule.toggle' | 'rule.update';
|
|
45
|
+
/** Who initiated. v0.4 is single-user local — always "local". v0.5+ adds users. */
|
|
46
|
+
user: string;
|
|
47
|
+
ruleId: string;
|
|
48
|
+
ruleName?: string;
|
|
49
|
+
/** Optional detail: source moment id, prior version, etc. */
|
|
50
|
+
details?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
export interface RulePreviewResult {
|
|
53
|
+
/** Number of historical traces evaluated. */
|
|
54
|
+
tracesEvaluated: number;
|
|
55
|
+
/** How many traces would have FAILED this proposed rule. */
|
|
56
|
+
wouldFail: number;
|
|
57
|
+
/** How many would have PASSED. */
|
|
58
|
+
wouldPass: number;
|
|
59
|
+
/** How many would have skipped (rule not applicable to context). */
|
|
60
|
+
wouldSkip: number;
|
|
61
|
+
/** First 5 example traces that would fail (with brief output preview). */
|
|
62
|
+
examples: Array<{
|
|
63
|
+
traceId: string;
|
|
64
|
+
agentName: string;
|
|
65
|
+
timestamp: string;
|
|
66
|
+
outputPreview: string;
|
|
67
|
+
}>;
|
|
68
|
+
/** Time window covered. */
|
|
69
|
+
windowSinceIso: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|