@iris-eval/mcp-server 0.4.5 → 0.5.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 +94 -44
- package/dist/audit-log-reader.d.ts +0 -2
- package/dist/audit-log-reader.js +5 -5
- package/dist/config/defaults.js +3 -3
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +32 -6
- package/dist/custom-rule-store.js +100 -38
- package/dist/dashboard/assets/index-BZZt8bVh.js +10 -0
- package/dist/dashboard/assets/index-UffZ-aEJ.css +1 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-greek.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-greek.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-vietnamese.woff2 +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/failures.d.ts +3 -0
- package/dist/dashboard/routes/failures.js +76 -0
- package/dist/dashboard/routes/index.d.ts +1 -0
- package/dist/dashboard/routes/index.js +1 -0
- package/dist/dashboard/routes/preferences.js +7 -2
- package/dist/dashboard/routes/rules.js +32 -14
- package/dist/dashboard/routes/traces.d.ts +12 -1
- package/dist/dashboard/routes/traces.js +90 -2
- package/dist/dashboard/seed-demo-data.d.ts +49 -0
- package/dist/dashboard/seed-demo-data.js +1080 -0
- package/dist/dashboard/server.js +124 -16
- package/dist/dashboard/validation.d.ts +74 -0
- package/dist/dashboard/validation.js +31 -2
- package/dist/eval/citation-verify/resolve.js +29 -0
- package/dist/eval/citation-verify/verifier.d.ts +1 -0
- package/dist/eval/citation-verify/verifier.js +12 -4
- package/dist/eval/engine.d.ts +15 -1
- package/dist/eval/engine.js +99 -15
- package/dist/eval/failure-rank.d.ts +14 -0
- package/dist/eval/failure-rank.js +44 -0
- package/dist/eval/rules/custom.d.ts +29 -1
- package/dist/eval/rules/custom.js +155 -19
- package/dist/eval/rules/regex-budget.d.ts +5 -0
- package/dist/eval/rules/regex-budget.js +100 -0
- package/dist/eval/rules/regex-sandbox.d.ts +26 -0
- package/dist/eval/rules/regex-sandbox.js +131 -0
- package/dist/eval/rules/relevance.d.ts +0 -2
- package/dist/eval/rules/relevance.js +6 -68
- package/dist/eval/rules/safety.d.ts +10 -0
- package/dist/eval/rules/safety.js +1377 -30
- package/dist/index.js +206 -19
- package/dist/middleware/rebinding-guard.d.ts +21 -0
- package/dist/middleware/rebinding-guard.js +77 -0
- package/dist/preferences.js +5 -10
- package/dist/self-test.d.ts +18 -0
- package/dist/self-test.js +329 -0
- package/dist/storage/migrations/005-normalize-created-at.d.ts +3 -0
- package/dist/storage/migrations/005-normalize-created-at.js +34 -0
- package/dist/storage/migrations/index.js +8 -1
- package/dist/storage/sqlite-adapter.d.ts +2 -0
- package/dist/storage/sqlite-adapter.js +93 -13
- package/dist/tools/delete-rule.d.ts +2 -1
- package/dist/tools/delete-rule.js +13 -4
- package/dist/tools/delete-trace.js +2 -1
- package/dist/tools/deploy-rule.d.ts +2 -1
- package/dist/tools/deploy-rule.js +29 -7
- package/dist/tools/evaluate-output.js +36 -9
- package/dist/tools/evaluate-with-llm-judge.js +2 -1
- package/dist/tools/get-traces.js +6 -2
- package/dist/tools/index.js +2 -2
- package/dist/tools/list-rules.js +2 -1
- package/dist/tools/log-trace.d.ts +51 -0
- package/dist/tools/log-trace.js +14 -2
- package/dist/tools/strict-input.d.ts +2 -0
- package/dist/tools/strict-input.js +35 -0
- package/dist/tools/verify-citations.js +7 -5
- package/dist/transport/http.js +24 -2
- package/dist/types/config.d.ts +7 -0
- package/dist/types/decision-moment.d.ts +12 -0
- package/dist/types/eval.d.ts +32 -0
- package/dist/types/query.d.ts +1 -1
- package/dist/utils/iris-home.d.ts +1 -0
- package/dist/utils/iris-home.js +21 -0
- package/dist/utils/write-atomic.d.ts +3 -0
- package/dist/utils/write-atomic.js +96 -0
- package/package.json +3 -2
- package/server.json +3 -3
- package/dist/dashboard/assets/index-B4Aw6ozt.css +0 -1
- package/dist/dashboard/assets/index-CIKsbEhq.js +0 -10
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* eval_type.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
+
import { createCustomRule } from '../eval/rules/custom.js';
|
|
14
15
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
16
|
+
import { strictInput } from './strict-input.js';
|
|
15
17
|
const CustomRuleDefinitionSchema = z.object({
|
|
16
18
|
name: z.string(),
|
|
17
19
|
type: z.enum([
|
|
@@ -28,7 +30,11 @@ const CustomRuleDefinitionSchema = z.object({
|
|
|
28
30
|
weight: z.number().optional(),
|
|
29
31
|
});
|
|
30
32
|
const inputSchema = {
|
|
31
|
-
|
|
33
|
+
// 80 mirrors the persisted store's cap (custom-rule-store.ts). The tool
|
|
34
|
+
// used to allow 120, so a 100-char name passed the tool schema and then
|
|
35
|
+
// surfaced the store's ZodError as a raw 500 (#332). One limit, enforced
|
|
36
|
+
// at the boundary, fails cleanly as a 400.
|
|
37
|
+
name: z.string().min(1).max(80).describe('Human-readable rule name (1-80 chars; used in eval results)'),
|
|
32
38
|
description: z
|
|
33
39
|
.string()
|
|
34
40
|
.max(500)
|
|
@@ -40,14 +46,14 @@ const inputSchema = {
|
|
|
40
46
|
severity: z
|
|
41
47
|
.enum(['low', 'medium', 'high', 'critical'])
|
|
42
48
|
.default('medium')
|
|
43
|
-
.describe('
|
|
49
|
+
.describe('What a FAILURE of this rule means. low/medium: informational — contributes to the weighted score only (plus dashboard sort + audit alerts). high/critical: hard-fail — a failing evaluation of this rule forces the overall passed=false regardless of the weighted score'),
|
|
44
50
|
definition: CustomRuleDefinitionSchema.describe('Check definition (regex, length, keyword, cost, or schema)'),
|
|
45
51
|
sourceMomentId: z
|
|
46
52
|
.string()
|
|
47
53
|
.optional()
|
|
48
54
|
.describe('Optional Decision Moment ID the rule was derived from (preserves workflow-inversion provenance)'),
|
|
49
55
|
};
|
|
50
|
-
export function registerDeployRuleTool(server, customRuleStore) {
|
|
56
|
+
export function registerDeployRuleTool(server, customRuleStore, evalEngine) {
|
|
51
57
|
server.registerTool('deploy_rule', {
|
|
52
58
|
title: 'Deploy Custom Rule',
|
|
53
59
|
description: [
|
|
@@ -63,11 +69,11 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
63
69
|
'',
|
|
64
70
|
"Don't use to VALIDATE a rule before committing — deploy writes immediately. Use the dashboard's preview endpoint (POST /api/v1/rules/custom/preview) for dry-run validation against sample output. Don't use to EDIT an existing rule — this call only creates; edits require a dedicated flow (coming in v0.5). To update a rule today: delete_rule then deploy_rule with the new definition.",
|
|
65
71
|
'',
|
|
66
|
-
'Parameters. name is 1-
|
|
72
|
+
'Parameters. name is 1-80 chars (Zod-enforced min/max — the same cap the persisted store applies); appears in eval_result rule_results so make it human-readable. description is optional, max 500 chars (used in dashboard tooltips). evalType determines WHEN the rule fires (must match the eval_type your evaluate_output calls use; e.g., a "completeness" rule fires on every evaluate_output where eval_type="completeness" OR eval_type="custom"). severity decides what a FAILURE of the rule does: low/medium failures only lower the weighted score (and drive dashboard sort + audit alerts); high/critical failures HARD-FAIL the evaluation — the overall `passed` is forced to false regardless of the weighted score, and the rule is listed in the response\'s `critical_failures`. Severity never changes the numeric score itself (that uses the rule\'s weight). definition.type and definition.config must match (e.g., regex_match needs config.pattern; cost_threshold needs config.max_cost; min_length needs config.min_length; max_length needs config.max_length; contains_keywords/excludes_keywords need config.keywords). Invalid configs are now REJECTED at deploy time with the offending field named, instead of deploying and then failing every evaluation. sourceMomentId is optional but recommended (preserves workflow-inversion provenance from Make-This-A-Rule composer). Defaults: severity="medium".',
|
|
67
73
|
'',
|
|
68
|
-
"Error modes. Throws 400 on invalid definition (Zod rejects — e.g., regex that fails safe-regex2 ReDoS check, or length > 1000 chars). Throws 400 on empty `name
|
|
74
|
+
"Error modes. Throws 400 on invalid definition (Zod rejects — e.g., regex that fails safe-regex2 ReDoS check, or length > 1000 chars). Throws 400 on empty `name` or `name` over 80 chars. Any evalType/definition.type combination is valid (a regex_match rule can enforce a safety policy; a max_length rule can express completeness) — there is no category/type mismatch error. Returns 429 when HTTP rate limit exceeded. File-write failures (disk full, read-only fs) propagate as 500; the audit log is best-effort and does not block deploy.",
|
|
69
75
|
].join('\n'),
|
|
70
|
-
inputSchema,
|
|
76
|
+
inputSchema: strictInput(inputSchema),
|
|
71
77
|
annotations: {
|
|
72
78
|
readOnlyHint: false,
|
|
73
79
|
destructiveHint: false,
|
|
@@ -75,16 +81,32 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
75
81
|
openWorldHint: false,
|
|
76
82
|
},
|
|
77
83
|
}, async (args) => {
|
|
84
|
+
// Server overrides the inner definition's `name` so it always matches
|
|
85
|
+
// the user-facing rule name — same normalization the dashboard's
|
|
86
|
+
// deploy route applies. Also keeps the tool's 80-char cap authoritative
|
|
87
|
+
// (an unchecked definition.name used to reach the store and surface its
|
|
88
|
+
// ZodError as a raw 500).
|
|
89
|
+
const definition = {
|
|
90
|
+
...args.definition,
|
|
91
|
+
name: args.name,
|
|
92
|
+
};
|
|
78
93
|
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
79
94
|
const rule = customRuleStore.deploy(LOCAL_TENANT, {
|
|
80
95
|
name: args.name,
|
|
81
96
|
description: args.description,
|
|
82
97
|
evalType: args.evalType,
|
|
83
98
|
severity: args.severity,
|
|
84
|
-
definition
|
|
99
|
+
definition,
|
|
85
100
|
sourceMomentId: args.sourceMomentId,
|
|
86
101
|
user: 'mcp',
|
|
87
102
|
});
|
|
103
|
+
// Register with the live engine so the rule fires on the very next
|
|
104
|
+
// evaluate_output call — the "activates immediately for the running
|
|
105
|
+
// process" this description promises. Previously only the dashboard's
|
|
106
|
+
// deploy route did this; MCP deploys silently waited for a restart.
|
|
107
|
+
// Registered under its rule id so delete_rule can hot-remove it.
|
|
108
|
+
// Severity rides along: high/critical makes the rule hard-failing.
|
|
109
|
+
evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition, rule.severity), rule.id);
|
|
88
110
|
return {
|
|
89
111
|
content: [
|
|
90
112
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { strictInput } from './strict-input.js';
|
|
3
4
|
const CustomRuleSchema = z.object({
|
|
4
5
|
name: z.string(),
|
|
5
6
|
type: z.enum([
|
|
@@ -11,11 +12,20 @@ const CustomRuleSchema = z.object({
|
|
|
11
12
|
});
|
|
12
13
|
const inputSchema = {
|
|
13
14
|
output: z.string().describe('The output text to evaluate (the agent\'s response that gets scored against rules)'),
|
|
14
|
-
|
|
15
|
+
// .optional() rather than .default('completeness') so the handler can tell
|
|
16
|
+
// "caller chose completeness" apart from "caller never chose" — the second
|
|
17
|
+
// case gets a note in the response saying safety rules did not run. The
|
|
18
|
+
// effective default is still completeness.
|
|
19
|
+
eval_type: z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']).optional().describe('Rule bundle to apply: completeness | relevance | safety | cost | custom — picks which built-in rules fire. Defaults to "completeness" when omitted (the response then carries a note that safety rules did not run)'),
|
|
15
20
|
expected: z.string().optional().describe('Expected output for comparison — REQUIRED when eval_type="relevance" (used as keyword-overlap target)'),
|
|
16
|
-
input: z.string().optional().describe('Original input for context — improves relevance scoring
|
|
21
|
+
input: z.string().optional().describe('Original input for context (the ask + any source material the agent was given) — improves relevance scoring and grounds the safety bundle\'s hallucination signals'),
|
|
17
22
|
trace_id: z.string().optional().describe('Link evaluation to a trace — surfaces this eval in the dashboard\'s trace drill-through'),
|
|
18
|
-
|
|
23
|
+
// .max(10): inline rules skip the deploy-time probe, and the engine runs
|
|
24
|
+
// rules synchronously — without a cap, one request carrying N sandbox-
|
|
25
|
+
// defeating regex rules stalls the server linearly in N (measured 9.3s at
|
|
26
|
+
// N=50). Ten is ample for per-call rules; persistent sets belong in
|
|
27
|
+
// deploy_rule, where deploy-time validation probes each pattern.
|
|
28
|
+
custom_rules: z.array(CustomRuleSchema).max(10).optional().describe('Custom evaluation rules, max 10 per call (deploy persistent rule sets via deploy_rule instead) — fires REGARDLESS of eval_type; pass eval_type="custom" if you want ONLY these'),
|
|
19
29
|
cost_usd: z.number().optional().describe('Cost in USD — only consulted when eval_type="cost" (compared against cost_threshold rules)'),
|
|
20
30
|
token_usage: z.object({
|
|
21
31
|
prompt_tokens: z.number().optional(),
|
|
@@ -33,17 +43,19 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
33
43
|
'',
|
|
34
44
|
'Behavior. Deterministic, in-process scoring — same inputs always produce the same result. Writes one eval_result row to Iris storage (linked to trace_id if provided; unlinked otherwise). No external network calls in heuristic mode (v0.4 adds an llm_as_judge eval_type that DOES call LLM APIs; see the separate evaluate_with_llm_judge tool for that). Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio. Runs in ~5-50ms for rule-based evaluation.',
|
|
35
45
|
'',
|
|
36
|
-
'Output shape. Returns JSON: `{ "id": "<uuid>", "score": 0..1, "passed": boolean, "rule_results": [{ "ruleName", "passed", "score", "message", "skipped?" }], "suggestions": string[], "rules_evaluated": number, "rules_skipped": number, "insufficient_data": boolean }`. `insufficient_data=true` means no applicable rules fired (e.g., safety eval with only cost data).',
|
|
46
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "eval_type": "<bundle that ran>", "score": 0..1, "passed": boolean, "critical_failures?": string[], "rule_results": [{ "ruleName", "passed", "score", "message", "skipped?" }], "suggestions": string[], "rules_evaluated": number, "rules_skipped": number, "insufficient_data": boolean, "note?": string }`. `insufficient_data=true` means no applicable rules fired (e.g., safety eval with only cost data). `note` appears only when eval_type was omitted, naming the defaulted bundle and that safety rules did not run.',
|
|
37
47
|
'',
|
|
38
|
-
'
|
|
48
|
+
'What `passed` means. `score` and `passed` answer different questions. `score` is the weighted average across the rules that ran — a 0..1 quality gradient. `passed` is the ship/no-ship verdict: true only when the score clears the pass threshold (default 0.7, configurable via config `eval.defaultThreshold`) AND no critical rule failed. Critical rules HARD-FAIL: if one fails, `passed` is false regardless of the weighted score, and the culprits are listed in `critical_failures`. The critical rules are the genuine safety violations — `no_pii`, `no_injection_patterns`, `no_blocklist_words` — plus any deployed custom rule with severity high/critical. A leaked SSN can never be averaged away by other rules passing.',
|
|
49
|
+
'',
|
|
50
|
+
'Use when you want a quality score on a specific output — typically after log_trace records the execution. Pass `eval_type` to route to the right rule bundle: `completeness` (length, sentence count, relevance to input), `relevance` (keyword overlap, topic consistency), `safety` (PII leak, prompt injection, hallucination markers, stub-output detection — pass `input` so the hallucination signals can cross-check the output against the material the agent was given), `cost` (budget threshold), or `custom` (bring your own rules via `custom_rules`).',
|
|
39
51
|
'',
|
|
40
52
|
'Don\'t use when the output is empty or has no applicable rules — the eval_type decides which rules apply, and invalid combinations return score=0 + insufficient_data=true (not an error, but not actionable). Don\'t use to VALIDATE JSON schemas directly (use your language\'s JSON Schema validator — Iris\'s `json_schema` custom rule type is for output-shape assertions, not arbitrary validation).',
|
|
41
53
|
'',
|
|
42
|
-
'Parameters. expected is REQUIRED when eval_type="relevance" (used as the comparison target for keyword overlap + topic consistency); ignored for other eval_types. cost_usd + token_usage are ONLY consulted when eval_type="cost" (ignored otherwise). custom_rules ALWAYS fires regardless of eval_type — pass eval_type="custom" if you want ONLY your rules to run (otherwise both your rules AND the eval_type bundle run together). trace_id is optional but recommended (linking the eval to its trace surfaces it in the dashboard\'s drill-through). input adds context to keyword-overlap relevance checks; ignored otherwise. Defaults: eval_type="completeness".',
|
|
54
|
+
'Parameters. expected is REQUIRED when eval_type="relevance" (used as the comparison target for keyword overlap + topic consistency); ignored for other eval_types. cost_usd + token_usage are ONLY consulted when eval_type="cost" (ignored otherwise). custom_rules ALWAYS fires regardless of eval_type — pass eval_type="custom" if you want ONLY your rules to run (otherwise both your rules AND the eval_type bundle run together). trace_id is optional but recommended (linking the eval to its trace surfaces it in the dashboard\'s drill-through). input adds context to keyword-overlap relevance checks AND grounds the safety bundle\'s hallucination signals (without it those signals stay silent rather than guess); ignored otherwise. Defaults: eval_type="completeness" — and when you rely on that default, the response carries a `note` reminding you that the safety bundle did not run.',
|
|
43
55
|
'',
|
|
44
|
-
'Error modes. Throws on malformed custom_rules (Zod rejects). 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.',
|
|
56
|
+
'Error modes. Throws on unknown argument names (strict schema — a misspelled argument is rejected with the valid argument list, never silently dropped). Throws on malformed custom_rules (Zod rejects) and on more than 10 custom_rules in one call (use deploy_rule for persistent rule sets). 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. A regex that exceeds the 100ms sandbox matching budget on a given output reports skipped with budgetExceeded=true instead of hanging the server (fail-open per rule — gate on that flag if you must fail closed).',
|
|
45
57
|
].join('\n'),
|
|
46
|
-
inputSchema,
|
|
58
|
+
inputSchema: strictInput(inputSchema),
|
|
47
59
|
annotations: {
|
|
48
60
|
readOnlyHint: false, // Writes an eval_result row
|
|
49
61
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
@@ -51,7 +63,12 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
51
63
|
openWorldHint: false, // No external network in heuristic mode; LLM-as-judge has its own tool with openWorldHint:true
|
|
52
64
|
},
|
|
53
65
|
}, async (args) => {
|
|
54
|
-
|
|
66
|
+
// Track omission explicitly: a caller who never chose a bundle gets
|
|
67
|
+
// the completeness default AND a note saying so — six of seven UAT
|
|
68
|
+
// personas read passed:true on PII-laden text with no hint that the
|
|
69
|
+
// safety bundle never ran.
|
|
70
|
+
const evalTypeOmitted = args.eval_type === undefined;
|
|
71
|
+
const evalType = (args.eval_type ?? 'completeness');
|
|
55
72
|
const result = evalEngine.evaluate(evalType, {
|
|
56
73
|
output: args.output,
|
|
57
74
|
expected: args.expected,
|
|
@@ -71,13 +88,23 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
71
88
|
type: 'text',
|
|
72
89
|
text: JSON.stringify({
|
|
73
90
|
id: result.id,
|
|
91
|
+
// Echo which bundle actually ran. Without this, a caller who
|
|
92
|
+
// omitted eval_type could not tell a "safety pass" from a
|
|
93
|
+
// completeness eval that never ran a single safety rule.
|
|
94
|
+
eval_type: result.eval_type,
|
|
74
95
|
score: result.score,
|
|
75
96
|
passed: result.passed,
|
|
97
|
+
...(result.critical_failures ? { critical_failures: result.critical_failures } : {}),
|
|
76
98
|
rule_results: result.rule_results,
|
|
77
99
|
suggestions: result.suggestions,
|
|
78
100
|
rules_evaluated: result.rules_evaluated,
|
|
79
101
|
rules_skipped: result.rules_skipped,
|
|
80
102
|
insufficient_data: result.insufficient_data,
|
|
103
|
+
...(evalTypeOmitted
|
|
104
|
+
? {
|
|
105
|
+
note: 'eval_type was omitted, so the default "completeness" bundle ran. Safety rules (PII, injection, blocklist, stub, hallucination) were NOT part of this evaluation — pass eval_type="safety" to run them.',
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
81
108
|
}),
|
|
82
109
|
},
|
|
83
110
|
],
|
|
@@ -3,6 +3,7 @@ import { LOCAL_TENANT } from '../types/tenant.js';
|
|
|
3
3
|
import { evaluateWithLLMJudge } from '../eval/llm-judge/evaluator.js';
|
|
4
4
|
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
5
|
import { generateEvalId } from '../utils/ids.js';
|
|
6
|
+
import { strictInput } from './strict-input.js';
|
|
6
7
|
const inputSchema = {
|
|
7
8
|
output: z.string().min(1).describe('The agent output text to evaluate'),
|
|
8
9
|
template: z
|
|
@@ -73,7 +74,7 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
73
74
|
'',
|
|
74
75
|
'Error modes. Throws when the required API key env var is missing. Throws when the estimated worst-case cost exceeds max_cost_usd (raise the cap or trim prompts). Throws LLMJudgeError on provider errors — kind=`auth` on 401/403, `rate_limit` on 429 (auto-retried once), `server_error` on 5xx, `timeout` on abort, `malformed_response` when the judge fails to emit valid JSON on both attempts. Throws "Unknown model" for unsupported model IDs — update src/eval/llm-judge/pricing.ts first.',
|
|
75
76
|
].join('\n'),
|
|
76
|
-
inputSchema,
|
|
77
|
+
inputSchema: strictInput(inputSchema),
|
|
77
78
|
annotations: {
|
|
78
79
|
readOnlyHint: false, // Writes eval_result; also spends money (external API cost)
|
|
79
80
|
destructiveHint: false, // Creates data; doesn't overwrite or delete
|
package/dist/tools/get-traces.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
|
+
import { strictInput } from './strict-input.js';
|
|
3
4
|
const inputSchema = {
|
|
4
5
|
agent_name: z.string().optional().describe('Filter by agent name — exact match (no wildcards in v0.4)'),
|
|
5
6
|
framework: z.string().optional().describe('Filter by agent framework — exact match (e.g., langchain, autogen)'),
|
|
@@ -7,7 +8,10 @@ const inputSchema = {
|
|
|
7
8
|
until: z.string().optional().describe('ISO timestamp upper bound — return traces with timestamp < this'),
|
|
8
9
|
min_score: z.number().optional().describe('Minimum eval score filter (0..1) — applied to LATEST eval per trace, not all evals'),
|
|
9
10
|
max_score: z.number().optional().describe('Maximum eval score filter (0..1) — applied to LATEST eval per trace'),
|
|
10
|
-
|
|
11
|
+
// Mirrors traceQuerySchema in dashboard/validation.ts — both capture paths
|
|
12
|
+
// (MCP tool, HTTP query) enforce the same 1..1000 bound. Unclamped, limit:-1
|
|
13
|
+
// meant "LIMIT -1" in SQLite, i.e. every row (#332).
|
|
14
|
+
limit: z.number().int().min(1).max(1000).default(50).describe('Results per page (default 50, max 1000 — values >1000 return 400)'),
|
|
11
15
|
offset: z.number().default(0).describe('Zero-based pagination offset — skip first N results'),
|
|
12
16
|
sort_by: z.enum(['timestamp', 'latency_ms', 'cost_usd']).default('timestamp').describe('Sort by timestamp | latency_ms | cost_usd (default timestamp)'),
|
|
13
17
|
sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order: asc | desc (default desc — most recent / highest first)'),
|
|
@@ -33,7 +37,7 @@ export function registerGetTracesTool(server, storage) {
|
|
|
33
37
|
'',
|
|
34
38
|
'Error modes. Returns 400 on invalid sort_by / sort_order (Zod enum). Returns 400 if limit > 1000. Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. Empty result with `total: 0` on no matches (not an error).',
|
|
35
39
|
].join('\n'),
|
|
36
|
-
inputSchema,
|
|
40
|
+
inputSchema: strictInput(inputSchema),
|
|
37
41
|
annotations: {
|
|
38
42
|
readOnlyHint: true, // Pure query: never writes, never deletes
|
|
39
43
|
destructiveHint: false, // Inverse of readOnly — trivially false
|
package/dist/tools/index.js
CHANGED
|
@@ -12,8 +12,8 @@ export function registerAllTools(server, storage, evalEngine, customRuleStore) {
|
|
|
12
12
|
registerEvaluateOutputTool(server, storage, evalEngine);
|
|
13
13
|
registerGetTracesTool(server, storage);
|
|
14
14
|
registerListRulesTool(server, customRuleStore);
|
|
15
|
-
registerDeployRuleTool(server, customRuleStore);
|
|
16
|
-
registerDeleteRuleTool(server, customRuleStore);
|
|
15
|
+
registerDeployRuleTool(server, customRuleStore, evalEngine);
|
|
16
|
+
registerDeleteRuleTool(server, customRuleStore, evalEngine);
|
|
17
17
|
registerDeleteTraceTool(server, storage);
|
|
18
18
|
registerEvaluateWithLLMJudgeTool(server, storage);
|
|
19
19
|
registerVerifyCitationsTool(server, storage);
|
package/dist/tools/list-rules.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
15
|
+
import { strictInput } from './strict-input.js';
|
|
15
16
|
const inputSchema = {
|
|
16
17
|
eval_type: z
|
|
17
18
|
.enum(['completeness', 'relevance', 'safety', 'cost', 'custom'])
|
|
@@ -42,7 +43,7 @@ export function registerListRulesTool(server, customRuleStore) {
|
|
|
42
43
|
'',
|
|
43
44
|
"Error modes. Returns empty list if the rule store file doesn't exist (first run). Returns 429 if HTTP rate limit exceeded. Never throws on valid input.",
|
|
44
45
|
].join('\n'),
|
|
45
|
-
inputSchema,
|
|
46
|
+
inputSchema: strictInput(inputSchema),
|
|
46
47
|
annotations: {
|
|
47
48
|
readOnlyHint: true,
|
|
48
49
|
destructiveHint: false,
|
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { IStorageAdapter } from '../types/query.js';
|
|
4
|
+
export declare const logTraceInputShape: {
|
|
5
|
+
agent_name: z.ZodString;
|
|
6
|
+
framework: z.ZodOptional<z.ZodString>;
|
|
7
|
+
input: z.ZodOptional<z.ZodString>;
|
|
8
|
+
output: z.ZodOptional<z.ZodString>;
|
|
9
|
+
tool_calls: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
10
|
+
tool_name: z.ZodString;
|
|
11
|
+
input: z.ZodOptional<z.ZodUnknown>;
|
|
12
|
+
output: z.ZodOptional<z.ZodUnknown>;
|
|
13
|
+
latency_ms: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
error: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>>>;
|
|
16
|
+
latency_ms: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
token_usage: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
prompt_tokens: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
completion_tokens: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
total_tokens: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
cost_usd: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
24
|
+
spans: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
25
|
+
span_id: z.ZodOptional<z.ZodString>;
|
|
26
|
+
parent_span_id: z.ZodOptional<z.ZodString>;
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
29
|
+
INTERNAL: "INTERNAL";
|
|
30
|
+
SERVER: "SERVER";
|
|
31
|
+
CLIENT: "CLIENT";
|
|
32
|
+
PRODUCER: "PRODUCER";
|
|
33
|
+
CONSUMER: "CONSUMER";
|
|
34
|
+
LLM: "LLM";
|
|
35
|
+
TOOL: "TOOL";
|
|
36
|
+
}>>;
|
|
37
|
+
status_code: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
UNSET: "UNSET";
|
|
39
|
+
OK: "OK";
|
|
40
|
+
ERROR: "ERROR";
|
|
41
|
+
}>>;
|
|
42
|
+
status_message: z.ZodOptional<z.ZodString>;
|
|
43
|
+
start_time: z.ZodString;
|
|
44
|
+
end_time: z.ZodOptional<z.ZodString>;
|
|
45
|
+
attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
46
|
+
events: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
timestamp: z.ZodString;
|
|
49
|
+
attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
50
|
+
}, z.core.$strip>>>;
|
|
51
|
+
}, z.core.$strip>>>;
|
|
52
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
53
|
+
};
|
|
3
54
|
export declare function registerLogTraceTool(server: McpServer, storage: IStorageAdapter): void;
|
package/dist/tools/log-trace.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { generateTraceId, generateSpanId } from '../utils/ids.js';
|
|
3
3
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
4
4
|
import { bestEffortExport } from '../otel/lazy.js';
|
|
5
|
+
import { strictInput } from './strict-input.js';
|
|
5
6
|
const ToolCallSchema = z.object({
|
|
6
7
|
tool_name: z.string(),
|
|
7
8
|
input: z.unknown().optional(),
|
|
@@ -30,7 +31,14 @@ const TokenUsageSchema = z.object({
|
|
|
30
31
|
completion_tokens: z.number().optional(),
|
|
31
32
|
total_tokens: z.number().optional(),
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
+
/*
|
|
35
|
+
* The log_trace input contract. Exported because POST /api/v1/traces
|
|
36
|
+
* (src/dashboard/routes/traces.ts) accepts the SAME body — one schema,
|
|
37
|
+
* two capture paths. Duplicating it there would let the tool and the
|
|
38
|
+
* HTTP endpoint drift apart silently; importing it means a field added
|
|
39
|
+
* here is accepted (and validated identically) on both.
|
|
40
|
+
*/
|
|
41
|
+
export const logTraceInputShape = {
|
|
34
42
|
agent_name: z.string().describe('Agent name — used for filtering in get_traces (e.g., "customer-support-bot")'),
|
|
35
43
|
framework: z.string().optional().describe('Agent framework identifier (e.g., langchain, autogen, custom)'),
|
|
36
44
|
input: z.string().optional().describe('Agent input text — the user prompt or upstream input that produced this output'),
|
|
@@ -63,7 +71,11 @@ export function registerLogTraceTool(server, storage) {
|
|
|
63
71
|
'',
|
|
64
72
|
'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.',
|
|
65
73
|
].join('\n'),
|
|
66
|
-
|
|
74
|
+
// Strict at the MCP boundary (unknown args rejected, not stripped).
|
|
75
|
+
// The dashboard's HTTP ingest builds its own schema FROM this shape
|
|
76
|
+
// (dashboard/validation.ts) and keeps default stripping there on
|
|
77
|
+
// purpose — it relies on it to discard a client-supplied trace_id.
|
|
78
|
+
inputSchema: strictInput(logTraceInputShape),
|
|
67
79
|
annotations: {
|
|
68
80
|
readOnlyHint: false, // Writes a row to storage
|
|
69
81
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/*
|
|
3
|
+
* Wraps a tool's input shape in a STRICT object schema so unknown argument
|
|
4
|
+
* names are REJECTED with an error that names the offending key(s) and
|
|
5
|
+
* lists the valid ones.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: a bare shape (or z.object()) silently STRIPS unknown
|
|
8
|
+
* keys. At the MCP tool boundary that is dangerous, not lenient — an LLM
|
|
9
|
+
* guessing an argument name is the normal case, not an edge case. Before
|
|
10
|
+
* this wrapper, `evaluate_output({ criteria: ["safety"], ... })` (a
|
|
11
|
+
* plausible guess) and `eval_typ: "safety"` (a one-character typo) both
|
|
12
|
+
* "succeeded": the arguments were dropped, the DEFAULT completeness bundle
|
|
13
|
+
* ran instead of the safety rules, and the response said passed:true on
|
|
14
|
+
* text containing real PII — with nothing indicating the arguments were
|
|
15
|
+
* ignored. Meanwhile a missing REQUIRED field produced a precise Zod
|
|
16
|
+
* error, so the failure mode was inconsistent as well as unsafe.
|
|
17
|
+
*
|
|
18
|
+
* The MCP SDK accepts a schema object (not just a raw shape) for
|
|
19
|
+
* inputSchema and validates tool calls through it, so the custom
|
|
20
|
+
* unrecognized-keys message below is exactly what the caller sees.
|
|
21
|
+
* Strictness also reaches tools/list: the generated JSON Schema carries
|
|
22
|
+
* additionalProperties:false, telling well-behaved clients up front.
|
|
23
|
+
*/
|
|
24
|
+
export function strictInput(shape) {
|
|
25
|
+
const validKeys = Object.keys(shape).join(', ');
|
|
26
|
+
return z.strictObject(shape, {
|
|
27
|
+
error: (issue) => issue.code === 'unrecognized_keys'
|
|
28
|
+
? `Unknown argument(s): ${issue.keys.map((k) => `"${k}"`).join(', ')}. ` +
|
|
29
|
+
`Valid arguments: ${validKeys}. ` +
|
|
30
|
+
'Unknown arguments are rejected rather than silently ignored, so a misspelled ' +
|
|
31
|
+
'argument name cannot change what gets evaluated — check the spelling against ' +
|
|
32
|
+
"the tool's input schema and retry."
|
|
33
|
+
: undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -3,6 +3,7 @@ import { LOCAL_TENANT } from '../types/tenant.js';
|
|
|
3
3
|
import { verifyCitations } from '../eval/citation-verify/verifier.js';
|
|
4
4
|
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
5
|
import { generateEvalId } from '../utils/ids.js';
|
|
6
|
+
import { strictInput } from './strict-input.js';
|
|
6
7
|
const inputSchema = {
|
|
7
8
|
output: z.string().min(1).describe('The agent output containing citations to verify'),
|
|
8
9
|
model: z
|
|
@@ -57,7 +58,7 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
57
58
|
'',
|
|
58
59
|
'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.',
|
|
59
60
|
'',
|
|
60
|
-
'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 /
|
|
61
|
+
'Output shape. Returns JSON: `{ "id": "<uuid>", "overall_score": 0..1|null, "passed": boolean, "total_citations_found": number, "total_resolved": number, "total_judged": 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 / judged`; `null` when nothing was judged (no resolvable citations, or every judge call failed). Infrastructure failures (cost cap, judge timeout/error, malformed verdict) leave a citation resolved-but-unjudged — reported per-citation via resolve_error, never scored as unsupported.',
|
|
61
62
|
'',
|
|
62
63
|
'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.',
|
|
63
64
|
"",
|
|
@@ -67,7 +68,7 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
67
68
|
'',
|
|
68
69
|
'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).',
|
|
69
70
|
].join('\n'),
|
|
70
|
-
inputSchema,
|
|
71
|
+
inputSchema: strictInput(inputSchema),
|
|
71
72
|
annotations: {
|
|
72
73
|
readOnlyHint: false, // Writes eval_result + spends money
|
|
73
74
|
destructiveHint: false, // Creates data; doesn't overwrite/delete
|
|
@@ -109,11 +110,11 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
109
110
|
passed: result.passed,
|
|
110
111
|
score,
|
|
111
112
|
message: result.overallScore === null
|
|
112
|
-
? `No
|
|
113
|
-
: `${result.totalSupported}/${result.
|
|
113
|
+
? `No citations judged (found ${result.totalCitationsFound}, resolved ${result.totalResolved}, judged 0)`
|
|
114
|
+
: `${result.totalSupported}/${result.totalJudged} judged sources supported the output`,
|
|
114
115
|
},
|
|
115
116
|
],
|
|
116
|
-
suggestions: result.passed ? [] : [`Only ${result.totalSupported}/${result.
|
|
117
|
+
suggestions: result.passed ? [] : [`Only ${result.totalSupported}/${result.totalJudged} judged sources actually supported the claim.`],
|
|
117
118
|
rules_evaluated: 1,
|
|
118
119
|
rules_skipped: 0,
|
|
119
120
|
insufficient_data: result.overallScore === null,
|
|
@@ -128,6 +129,7 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
128
129
|
passed: result.passed,
|
|
129
130
|
total_citations_found: result.totalCitationsFound,
|
|
130
131
|
total_resolved: result.totalResolved,
|
|
132
|
+
total_judged: result.totalJudged,
|
|
131
133
|
total_supported: result.totalSupported,
|
|
132
134
|
total_cost_usd: result.totalCostUsd,
|
|
133
135
|
citations: result.citations.map((c) => ({
|
package/dist/transport/http.js
CHANGED
|
@@ -56,9 +56,31 @@ export async function createHttpTransport(mcpServer, config, logger) {
|
|
|
56
56
|
* `127.0.0.1:0` and reject every real request with a 403 that looks
|
|
57
57
|
* exactly like an attack. Routes are registered immediately after, and
|
|
58
58
|
* the port is not discoverable by any client until this function returns.
|
|
59
|
+
*
|
|
60
|
+
* The callback MUST inspect its error argument. Express 5 wires the
|
|
61
|
+
* listen callback as `server.once('error', done)` as well as the
|
|
62
|
+
* listening callback — so on EADDRINUSE it is invoked WITH the error.
|
|
63
|
+
* Ignoring that argument resolved this promise on a server that never
|
|
64
|
+
* bound: the caller then logged "HTTP transport listening on <port>"
|
|
65
|
+
* while another process owned the port, and the process idled forever.
|
|
66
|
+
* A CI health poll got 200 from the OTHER instance and shipped
|
|
67
|
+
* evaluations to a stranger's database. A bind failure must reject,
|
|
68
|
+
* name the port, and take the process down nonzero.
|
|
59
69
|
*/
|
|
60
|
-
const httpServer = await new Promise((resolve) => {
|
|
61
|
-
const server = app.listen(config.transport.port, config.transport.host, () =>
|
|
70
|
+
const httpServer = await new Promise((resolve, reject) => {
|
|
71
|
+
const server = app.listen(config.transport.port, config.transport.host, (err) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
const bind = `${config.transport.host}:${config.transport.port}`;
|
|
74
|
+
const code = err.code;
|
|
75
|
+
reject(code === 'EADDRINUSE'
|
|
76
|
+
? new Error(`HTTP transport failed to start: port ${config.transport.port} is already in use ` +
|
|
77
|
+
`(EADDRINUSE on ${bind}). Another process — possibly another iris instance — owns it. ` +
|
|
78
|
+
`Pass --port <other> (or set IRIS_PORT) or stop the other process.`)
|
|
79
|
+
: new Error(`HTTP transport failed to bind ${bind}: ${err.message}`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
resolve(server);
|
|
83
|
+
});
|
|
62
84
|
});
|
|
63
85
|
const address = httpServer.address();
|
|
64
86
|
const port = typeof address === 'object' && address ? address.port : config.transport.port;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface IrisConfig {
|
|
|
15
15
|
dashboard: {
|
|
16
16
|
enabled: boolean;
|
|
17
17
|
port: number;
|
|
18
|
+
/**
|
|
19
|
+
* Bind address. Defaults to loopback: the dashboard is unauthenticated
|
|
20
|
+
* by default (security.apiKey is undefined) and serves the full trace
|
|
21
|
+
* history, so binding it to every interface exposes agent inputs and
|
|
22
|
+
* outputs to the local network. Set explicitly to share it.
|
|
23
|
+
*/
|
|
24
|
+
host: string;
|
|
18
25
|
};
|
|
19
26
|
eval: {
|
|
20
27
|
defaultThreshold: number;
|
|
@@ -120,3 +120,15 @@ export interface MomentQueryResult {
|
|
|
120
120
|
limit: number;
|
|
121
121
|
offset: number;
|
|
122
122
|
}
|
|
123
|
+
export interface RankedFailure extends DecisionMoment {
|
|
124
|
+
/** Severity × recency-decay blend, 0-1. Higher = shown first. */
|
|
125
|
+
rankScore: number;
|
|
126
|
+
}
|
|
127
|
+
export interface FailureQueryResult {
|
|
128
|
+
failures: RankedFailure[];
|
|
129
|
+
/** How many recent traces were scanned to build the list. */
|
|
130
|
+
scanned: number;
|
|
131
|
+
/** Total traces matching the filter (pre-scan-cap). */
|
|
132
|
+
total: number;
|
|
133
|
+
limit: number;
|
|
134
|
+
}
|
package/dist/types/eval.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ export interface EvalRule {
|
|
|
4
4
|
description: string;
|
|
5
5
|
evalType: EvalType;
|
|
6
6
|
weight: number;
|
|
7
|
+
/**
|
|
8
|
+
* Hard-fail marker. When a critical rule FAILS (and was not skipped), the
|
|
9
|
+
* overall eval reports passed=false regardless of the weighted score.
|
|
10
|
+
*
|
|
11
|
+
* Exists because the weighted average routinely outvotes a genuine
|
|
12
|
+
* violation: an output leaking a real SSN failed no_pii while the other
|
|
13
|
+
* safety rules passed, scoring ~0.765 — above the 0.7 threshold — so the
|
|
14
|
+
* one field every CI gate reads said passed:true about the product's
|
|
15
|
+
* flagship failure scenario. The score stays a quality gradient; `passed`
|
|
16
|
+
* is the verdict, and a critical violation must never be averaged away.
|
|
17
|
+
*/
|
|
18
|
+
critical?: boolean;
|
|
7
19
|
evaluate(context: EvalContext): EvalRuleResult;
|
|
8
20
|
}
|
|
9
21
|
export interface EvalContext {
|
|
@@ -23,6 +35,17 @@ export interface EvalContext {
|
|
|
23
35
|
costUsd?: number;
|
|
24
36
|
metadata?: Record<string, unknown>;
|
|
25
37
|
customConfig?: Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Per-evaluation regex circuit breaker, initialized by the engine (never
|
|
40
|
+
* by callers). Each sandbox budget breach increments `breaches`; once it
|
|
41
|
+
* reaches the cap, remaining regex rules in the SAME evaluation skip
|
|
42
|
+
* without running. Bounds how long a single hostile output can stall a
|
|
43
|
+
* request: without it, N regex rules × (budget + worker respawn) of
|
|
44
|
+
* main-thread stall scale linearly with N.
|
|
45
|
+
*/
|
|
46
|
+
regexBudget?: {
|
|
47
|
+
breaches: number;
|
|
48
|
+
};
|
|
26
49
|
}
|
|
27
50
|
export interface EvalRuleResult {
|
|
28
51
|
ruleName: string;
|
|
@@ -31,6 +54,8 @@ export interface EvalRuleResult {
|
|
|
31
54
|
message: string;
|
|
32
55
|
skipped?: boolean;
|
|
33
56
|
skipReason?: string;
|
|
57
|
+
configInvalid?: boolean;
|
|
58
|
+
budgetExceeded?: boolean;
|
|
34
59
|
}
|
|
35
60
|
export interface EvalResult {
|
|
36
61
|
id: string;
|
|
@@ -46,6 +71,13 @@ export interface EvalResult {
|
|
|
46
71
|
rules_evaluated?: number;
|
|
47
72
|
rules_skipped?: number;
|
|
48
73
|
insufficient_data?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Names of critical rules that failed (present only when non-empty).
|
|
76
|
+
* Any entry here forces passed=false regardless of the weighted score —
|
|
77
|
+
* this field is how a caller tells "failed the quality bar" apart from
|
|
78
|
+
* "committed a hard violation".
|
|
79
|
+
*/
|
|
80
|
+
critical_failures?: string[];
|
|
49
81
|
}
|
|
50
82
|
export type CustomRuleType = 'regex_match' | 'regex_no_match' | 'min_length' | 'max_length' | 'contains_keywords' | 'excludes_keywords' | 'json_schema' | 'cost_threshold';
|
|
51
83
|
export interface CustomRuleDefinition {
|
package/dist/types/query.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface TraceQueryResult {
|
|
|
23
23
|
limit: number;
|
|
24
24
|
offset: number;
|
|
25
25
|
}
|
|
26
|
-
export type EvalStatsPeriod = '24h' | '7d' | '30d' | 'all';
|
|
26
|
+
export type EvalStatsPeriod = '24h' | '2d' | '7d' | '14d' | '30d' | '60d' | '90d' | '180d' | 'all';
|
|
27
27
|
export interface EvalStats {
|
|
28
28
|
passRate: number;
|
|
29
29
|
avgScore: number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function irisHome(): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
/*
|
|
4
|
+
* Single resolver for the iris home directory (default: ~/.iris).
|
|
5
|
+
*
|
|
6
|
+
* Every per-user file iris touches lives under this directory — the
|
|
7
|
+
* SQLite DB default, config.json, custom-rules.json, audit.log,
|
|
8
|
+
* preferences.json. Before this helper each module joined
|
|
9
|
+
* homedir() + '.iris' itself, which meant there was no way to point a
|
|
10
|
+
* spawned server at a scratch directory: the E2E suite isolated the DB
|
|
11
|
+
* via IRIS_DB_PATH but still wiped the real audit.log, deployed test
|
|
12
|
+
* rules into the real custom-rules.json, and overwrote the real
|
|
13
|
+
* preferences.json on every run.
|
|
14
|
+
*
|
|
15
|
+
* IRIS_HOME redirects all of them at once. Read at call time — not
|
|
16
|
+
* module load — so a test harness that sets the env var before
|
|
17
|
+
* spawning (or between in-process calls) always wins.
|
|
18
|
+
*/
|
|
19
|
+
export function irisHome() {
|
|
20
|
+
return process.env.IRIS_HOME ?? join(homedir(), '.iris');
|
|
21
|
+
}
|