@iris-eval/mcp-server 0.4.0 → 0.4.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 +2 -2
- package/dist/custom-rule-store.d.ts +18 -10
- package/dist/custom-rule-store.js +81 -45
- package/dist/dashboard/assets/{index-BEG5FYWH.css → index-B4Aw6ozt.css} +1 -1
- package/dist/dashboard/assets/index-YnFsPfd6.js +12 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/rules.js +18 -5
- package/dist/eval/citation-verify/resolve.d.ts +7 -0
- package/dist/eval/citation-verify/resolve.js +42 -7
- package/dist/index.js +5 -2
- package/dist/lib/claims.d.ts +111 -0
- package/dist/lib/claims.js +52 -0
- package/dist/middleware/auth.js +12 -1
- package/dist/middleware/cors.js +7 -1
- package/dist/middleware/tenant.d.ts +5 -3
- package/dist/tools/delete-rule.js +7 -1
- package/dist/tools/delete-trace.js +4 -0
- package/dist/tools/deploy-rule.js +7 -1
- package/dist/tools/evaluate-output.js +12 -8
- package/dist/tools/evaluate-with-llm-judge.js +4 -0
- package/dist/tools/get-traces.js +15 -11
- package/dist/tools/list-rules.js +9 -1
- package/dist/tools/log-trace.js +15 -11
- package/dist/tools/verify-citations.js +4 -0
- package/package.json +5 -1
- package/server.json +2 -2
- package/dist/dashboard/assets/index-D9JHfSB2.js +0 -12
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>Iris — Agent Eval & Observability</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-YnFsPfd6.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B4Aw6ozt.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -36,12 +36,22 @@ const PreviewSchema = z.object({
|
|
|
36
36
|
maxTraces: z.coerce.number().int().min(1).max(5000).default(1000),
|
|
37
37
|
});
|
|
38
38
|
export function registerRuleRoutes(router, storage, opts) {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/*
|
|
40
|
+
* Every /rules/custom route resolves the tenant via requireTenant(req)
|
|
41
|
+
* and threads it through to the store. In OSS the tenant middleware
|
|
42
|
+
* always sets LOCAL_TENANT, so all rules continue to live at
|
|
43
|
+
* ~/.iris/custom-rules.json (the v0.4 path — zero migration). In Cloud,
|
|
44
|
+
* each tenant's rules will live in their own file (per-tenant partition)
|
|
45
|
+
* and writes will only ever touch the resolved tenant's data.
|
|
46
|
+
*/
|
|
47
|
+
router.get('/rules/custom', (req, res) => {
|
|
48
|
+
const tenantId = requireTenant(req);
|
|
49
|
+
const rules = opts.customRuleStore.list(tenantId);
|
|
41
50
|
res.json({ rules });
|
|
42
51
|
});
|
|
43
52
|
router.post('/rules/custom', async (req, res) => {
|
|
44
53
|
try {
|
|
54
|
+
const tenantId = requireTenant(req);
|
|
45
55
|
const input = DeploySchema.parse(req.body);
|
|
46
56
|
// Server overrides the inner definition's `name` so it always matches the
|
|
47
57
|
// user-facing rule name. Avoids confusion when the rule name and the
|
|
@@ -50,7 +60,7 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
50
60
|
...input.definition,
|
|
51
61
|
name: input.name,
|
|
52
62
|
};
|
|
53
|
-
const rule = opts.customRuleStore.deploy({
|
|
63
|
+
const rule = opts.customRuleStore.deploy(tenantId, {
|
|
54
64
|
name: input.name,
|
|
55
65
|
description: input.description,
|
|
56
66
|
evalType: input.evalType,
|
|
@@ -59,7 +69,9 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
59
69
|
sourceMomentId: input.sourceMomentId,
|
|
60
70
|
});
|
|
61
71
|
// Register the new rule with the live engine so it fires on subsequent
|
|
62
|
-
// evaluate_output calls without requiring a server restart.
|
|
72
|
+
// evaluate_output calls without requiring a server restart. The engine
|
|
73
|
+
// is process-global in v0.4 — Cloud multi-tenant engine wiring is a
|
|
74
|
+
// v0.5 architectural item.
|
|
63
75
|
opts.evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition));
|
|
64
76
|
res.status(201).json({ rule });
|
|
65
77
|
}
|
|
@@ -72,7 +84,8 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
72
84
|
}
|
|
73
85
|
});
|
|
74
86
|
router.delete('/rules/custom/:id', (req, res) => {
|
|
75
|
-
const
|
|
87
|
+
const tenantId = requireTenant(req);
|
|
88
|
+
const removed = opts.customRuleStore.delete(tenantId, req.params.id);
|
|
76
89
|
if (!removed) {
|
|
77
90
|
res.status(404).json({ error: 'Rule not found' });
|
|
78
91
|
return;
|
|
@@ -22,5 +22,12 @@ export declare class CitationResolveError extends Error {
|
|
|
22
22
|
constructor(message: string, kind: 'bad_scheme' | 'ssrf' | 'not_allowed_domain' | 'timeout' | 'too_large' | 'bad_status' | 'redirect_loop' | 'not_text' | 'fetch_disabled', details?: string | undefined);
|
|
23
23
|
}
|
|
24
24
|
export declare function isSafeHost(host: string): boolean;
|
|
25
|
+
type DnsLookupAll = (host: string) => Promise<Array<{
|
|
26
|
+
address: string;
|
|
27
|
+
family: 4 | 6;
|
|
28
|
+
}>>;
|
|
29
|
+
export declare function __setDnsLookupForTests(impl: DnsLookupAll | null): void;
|
|
30
|
+
export declare function resolveAndCheckHost(host: string): Promise<void>;
|
|
25
31
|
export declare function __clearCitationCacheForTests(): void;
|
|
26
32
|
export declare function resolveSource(identifier: string, opts: ResolveOptions): Promise<ResolvedSource>;
|
|
33
|
+
export {};
|
|
@@ -7,15 +7,25 @@
|
|
|
7
7
|
// 1. Scheme allowlist — http/https only; refuse file:/javascript:/etc.
|
|
8
8
|
// 2. SSRF host check — refuse localhost, link-local, private ranges,
|
|
9
9
|
// and cloud metadata (AWS/GCP/Azure/DigitalOcean) IP literals.
|
|
10
|
-
// 3.
|
|
10
|
+
// 3. DNS pre-resolve — every public hostname is resolved via
|
|
11
|
+
// dns.lookup({all:true}) and EVERY returned IP is re-checked against
|
|
12
|
+
// the IP blocklist. Defeats DNS-rebinding via public records pointing
|
|
13
|
+
// at private space (e.g. `*.localtest.me` resolving to 127.0.0.1).
|
|
14
|
+
// Residual TOCTOU window between this lookup and the socket connect
|
|
15
|
+
// is acknowledged — closing it requires a custom undici dispatcher;
|
|
16
|
+
// queued for follow-up if exploitation is observed.
|
|
17
|
+
// 4. Optional domain allowlist — IRIS_CITATION_DOMAINS=doi.org,arxiv.org
|
|
11
18
|
// restricts to a curated set; empty/unset = open web (still SSRF-guarded).
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
19
|
+
// 5. Timeout + size cap — 10s default, 5MB cap on response body.
|
|
20
|
+
// 6. Redirect chase cap — follow max 3 redirects, each re-checked.
|
|
21
|
+
// 7. Cache — in-process LRU (100 entries) so retries don't re-fetch.
|
|
15
22
|
//
|
|
16
23
|
// This is opt-in: calls require passing {allowFetch: true} so an agent
|
|
17
24
|
// can't trick Iris into fetching random URLs without operator consent
|
|
18
25
|
// (consent granted via tool param or env IRIS_CITATION_ALLOW_FETCH=1).
|
|
26
|
+
import { lookup as dnsLookupCb } from 'node:dns';
|
|
27
|
+
import { promisify } from 'node:util';
|
|
28
|
+
const dnsLookupAll = promisify(dnsLookupCb);
|
|
19
29
|
export class CitationResolveError extends Error {
|
|
20
30
|
kind;
|
|
21
31
|
details;
|
|
@@ -76,6 +86,33 @@ export function isSafeHost(host) {
|
|
|
76
86
|
}
|
|
77
87
|
return true;
|
|
78
88
|
}
|
|
89
|
+
let dnsLookupImpl = (host) => dnsLookupAll(host, { all: true });
|
|
90
|
+
export function __setDnsLookupForTests(impl) {
|
|
91
|
+
dnsLookupImpl = impl ?? ((host) => dnsLookupAll(host, { all: true }));
|
|
92
|
+
}
|
|
93
|
+
export async function resolveAndCheckHost(host) {
|
|
94
|
+
if (!isSafeHost(host)) {
|
|
95
|
+
throw new CitationResolveError(`Refusing SSRF-blocked host: ${host}`, 'ssrf', host);
|
|
96
|
+
}
|
|
97
|
+
// IP literals already passed isSafeHost — skip DNS (would just re-resolve to self).
|
|
98
|
+
if (isIpv4(host) || isIpv6(host))
|
|
99
|
+
return;
|
|
100
|
+
let addresses;
|
|
101
|
+
try {
|
|
102
|
+
addresses = await dnsLookupImpl(host);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
throw new CitationResolveError(`DNS resolution failed for ${host}: ${err instanceof Error ? err.message : String(err)}`, 'ssrf', host);
|
|
106
|
+
}
|
|
107
|
+
if (addresses.length === 0) {
|
|
108
|
+
throw new CitationResolveError(`DNS returned no addresses for ${host}`, 'ssrf', host);
|
|
109
|
+
}
|
|
110
|
+
for (const { address } of addresses) {
|
|
111
|
+
if (!isSafeHost(address)) {
|
|
112
|
+
throw new CitationResolveError(`Refusing SSRF-blocked address ${address} (resolved from ${host})`, 'ssrf', host);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
79
116
|
function matchesAllowlist(host, allowlist) {
|
|
80
117
|
if (!allowlist || allowlist.length === 0)
|
|
81
118
|
return true;
|
|
@@ -127,9 +164,7 @@ async function doFetch(url, opts, redirectsLeft) {
|
|
|
127
164
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
128
165
|
throw new CitationResolveError(`Refusing non-http(s) scheme: ${parsed.protocol}`, 'bad_scheme', parsed.protocol);
|
|
129
166
|
}
|
|
130
|
-
|
|
131
|
-
throw new CitationResolveError(`Refusing SSRF-blocked host: ${parsed.hostname}`, 'ssrf', parsed.hostname);
|
|
132
|
-
}
|
|
167
|
+
await resolveAndCheckHost(parsed.hostname);
|
|
133
168
|
if (!matchesAllowlist(parsed.hostname, opts.domainAllowlist)) {
|
|
134
169
|
throw new CitationResolveError(`Host ${parsed.hostname} not in IRIS_CITATION_DOMAINS allowlist`, 'not_allowed_domain', parsed.hostname);
|
|
135
170
|
}
|
package/dist/index.js
CHANGED
|
@@ -129,12 +129,15 @@ async function main() {
|
|
|
129
129
|
// Load deployed custom rules from ~/.iris/custom-rules.json (B3 — workflow inversion).
|
|
130
130
|
// Each enabled rule is registered with the engine under its evalType so it fires on
|
|
131
131
|
// every evaluate_output call of that category. Persistence via custom-rule-store.
|
|
132
|
-
|
|
132
|
+
// OSS single-tenant: register rules under LOCAL_TENANT only. Cloud multi-tenant
|
|
133
|
+
// engine wiring is a v0.5 architectural item (the engine is a process singleton
|
|
134
|
+
// and would need per-tenant rule registration).
|
|
135
|
+
const enabled = customRuleStore.enabledRules(LOCAL_TENANT);
|
|
133
136
|
for (const rule of enabled) {
|
|
134
137
|
evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition));
|
|
135
138
|
}
|
|
136
139
|
if (enabled.length > 0) {
|
|
137
|
-
logger.info(`Loaded ${enabled.length} deployed custom rule(s) from ${customRuleStore.
|
|
140
|
+
logger.info(`Loaded ${enabled.length} deployed custom rule(s) from ${customRuleStore.pathFor(LOCAL_TENANT)}`);
|
|
138
141
|
}
|
|
139
142
|
const httpServers = [];
|
|
140
143
|
// Run data retention cleanup on startup.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export declare const CLAIMS: {
|
|
2
|
+
$schema: string;
|
|
3
|
+
brand: {
|
|
4
|
+
categoryName: string;
|
|
5
|
+
coinedTerms: string[];
|
|
6
|
+
npmPackage: string;
|
|
7
|
+
publicRepoUrl: string;
|
|
8
|
+
securityEmail: string;
|
|
9
|
+
supportEmail: string;
|
|
10
|
+
tagline: string;
|
|
11
|
+
websiteUrl: string;
|
|
12
|
+
};
|
|
13
|
+
evalRules: {
|
|
14
|
+
builtInCount: number;
|
|
15
|
+
categories: string[];
|
|
16
|
+
categoryCount: number;
|
|
17
|
+
hallucinationMarkers: number;
|
|
18
|
+
injectionPatterns: number;
|
|
19
|
+
names: string[];
|
|
20
|
+
piiPatterns: number;
|
|
21
|
+
stubMarkers: string[];
|
|
22
|
+
};
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
generatedFromCommit: string;
|
|
25
|
+
generatorVersion: string;
|
|
26
|
+
llmJudgeTemplates: {
|
|
27
|
+
count: number;
|
|
28
|
+
names: string[];
|
|
29
|
+
supportedModels: string[];
|
|
30
|
+
supportedProviders: string[];
|
|
31
|
+
};
|
|
32
|
+
mcpTools: {
|
|
33
|
+
annotations: {
|
|
34
|
+
destructiveHintCount: number;
|
|
35
|
+
openWorldHintCount: number;
|
|
36
|
+
readOnlyHintCount: number;
|
|
37
|
+
};
|
|
38
|
+
count: number;
|
|
39
|
+
names: string[];
|
|
40
|
+
};
|
|
41
|
+
release: {
|
|
42
|
+
currentReleaseDate: string;
|
|
43
|
+
currentReleaseVersion: string;
|
|
44
|
+
nextPlannedScope: string;
|
|
45
|
+
nextPlannedVersion: string;
|
|
46
|
+
};
|
|
47
|
+
schemaVersion: number;
|
|
48
|
+
tests: {
|
|
49
|
+
integration: {
|
|
50
|
+
failed: null;
|
|
51
|
+
passed: null;
|
|
52
|
+
total: null;
|
|
53
|
+
};
|
|
54
|
+
playwrightE2E: {
|
|
55
|
+
browsers: never[];
|
|
56
|
+
failed: null;
|
|
57
|
+
passed: null;
|
|
58
|
+
total: null;
|
|
59
|
+
};
|
|
60
|
+
totalCombined: number;
|
|
61
|
+
vitestDashboard: {
|
|
62
|
+
failed: number;
|
|
63
|
+
passed: number;
|
|
64
|
+
total: number;
|
|
65
|
+
};
|
|
66
|
+
vitestRoot: {
|
|
67
|
+
failed: number;
|
|
68
|
+
passed: number;
|
|
69
|
+
total: number;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
version: {
|
|
73
|
+
dashboardPackage: string;
|
|
74
|
+
initPackage: string;
|
|
75
|
+
langchainPackage: string;
|
|
76
|
+
mcpServer: string;
|
|
77
|
+
websitePackage: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export declare const VERSION_MCP_SERVER: string;
|
|
81
|
+
export declare const VERSION_LANGCHAIN_PACKAGE: string | null;
|
|
82
|
+
export declare const VERSION_WEBSITE_PACKAGE: string | null;
|
|
83
|
+
export declare const VERSION_DASHBOARD_PACKAGE: string | null;
|
|
84
|
+
export declare const TEST_COUNT_VITEST_ROOT: number | null;
|
|
85
|
+
export declare const TEST_COUNT_VITEST_DASHBOARD: number | null;
|
|
86
|
+
export declare const TEST_COUNT_INTEGRATION: number | null;
|
|
87
|
+
export declare const TEST_COUNT_PLAYWRIGHT_E2E: number | null;
|
|
88
|
+
export declare const TEST_COUNT_TOTAL: number | null;
|
|
89
|
+
export declare const MCP_TOOL_COUNT: number;
|
|
90
|
+
export declare const MCP_TOOL_NAMES: readonly string[];
|
|
91
|
+
export declare const RULE_COUNT_BUILT_IN: number;
|
|
92
|
+
export declare const RULE_CATEGORIES: readonly string[];
|
|
93
|
+
export declare const RULE_CATEGORY_COUNT: number;
|
|
94
|
+
export declare const RULE_NAMES: readonly string[];
|
|
95
|
+
export declare const PII_PATTERN_COUNT: number | null;
|
|
96
|
+
export declare const INJECTION_PATTERN_COUNT: number | null;
|
|
97
|
+
export declare const HALLUCINATION_MARKER_COUNT: number | null;
|
|
98
|
+
export declare const LLM_JUDGE_TEMPLATE_COUNT: number;
|
|
99
|
+
export declare const LLM_JUDGE_TEMPLATE_NAMES: readonly string[];
|
|
100
|
+
export declare const TAGLINE: string;
|
|
101
|
+
export declare const CATEGORY_NAME: string;
|
|
102
|
+
export declare const COINED_TERMS: readonly string[];
|
|
103
|
+
export declare const WEBSITE_URL: string;
|
|
104
|
+
export declare const PUBLIC_REPO_URL: string;
|
|
105
|
+
export declare const NPM_PACKAGE: string;
|
|
106
|
+
export declare const SUPPORT_EMAIL: string;
|
|
107
|
+
export declare const SECURITY_EMAIL: string;
|
|
108
|
+
export declare const CURRENT_RELEASE_VERSION: string | null;
|
|
109
|
+
export declare const CURRENT_RELEASE_DATE: string | null;
|
|
110
|
+
export declare const NEXT_PLANNED_VERSION: string | null;
|
|
111
|
+
export declare const NEXT_PLANNED_SCOPE: string | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Truthbase reader — single import surface for facts about Iris.
|
|
2
|
+
//
|
|
3
|
+
// Source of truth: iris/.claims.json (regenerated from canonical artifacts
|
|
4
|
+
// via `npm run claims:generate`). Surfaces import the named consts below
|
|
5
|
+
// rather than hardcoding values inline.
|
|
6
|
+
//
|
|
7
|
+
// When a fact changes (a new test lands, a new MCP tool registers, the
|
|
8
|
+
// version bumps), the regenerator updates .claims.json on the next commit
|
|
9
|
+
// and every surface that imports from this reader updates simultaneously.
|
|
10
|
+
// No drift.
|
|
11
|
+
import claimsRaw from '../../.claims.json' with { type: 'json' };
|
|
12
|
+
// Re-export the raw object for surfaces that need composite values.
|
|
13
|
+
export const CLAIMS = claimsRaw;
|
|
14
|
+
// Versions
|
|
15
|
+
export const VERSION_MCP_SERVER = claimsRaw.version.mcpServer;
|
|
16
|
+
export const VERSION_LANGCHAIN_PACKAGE = claimsRaw.version.langchainPackage;
|
|
17
|
+
export const VERSION_WEBSITE_PACKAGE = claimsRaw.version.websitePackage;
|
|
18
|
+
export const VERSION_DASHBOARD_PACKAGE = claimsRaw.version.dashboardPackage;
|
|
19
|
+
// Tests
|
|
20
|
+
export const TEST_COUNT_VITEST_ROOT = claimsRaw.tests.vitestRoot.total;
|
|
21
|
+
export const TEST_COUNT_VITEST_DASHBOARD = claimsRaw.tests.vitestDashboard.total;
|
|
22
|
+
export const TEST_COUNT_INTEGRATION = claimsRaw.tests.integration.total;
|
|
23
|
+
export const TEST_COUNT_PLAYWRIGHT_E2E = claimsRaw.tests.playwrightE2E.total;
|
|
24
|
+
export const TEST_COUNT_TOTAL = claimsRaw.tests.totalCombined;
|
|
25
|
+
// MCP tools
|
|
26
|
+
export const MCP_TOOL_COUNT = claimsRaw.mcpTools.count;
|
|
27
|
+
export const MCP_TOOL_NAMES = claimsRaw.mcpTools.names;
|
|
28
|
+
// Eval rules
|
|
29
|
+
export const RULE_COUNT_BUILT_IN = claimsRaw.evalRules.builtInCount;
|
|
30
|
+
export const RULE_CATEGORIES = claimsRaw.evalRules.categories;
|
|
31
|
+
export const RULE_CATEGORY_COUNT = claimsRaw.evalRules.categoryCount;
|
|
32
|
+
export const RULE_NAMES = claimsRaw.evalRules.names;
|
|
33
|
+
export const PII_PATTERN_COUNT = claimsRaw.evalRules.piiPatterns;
|
|
34
|
+
export const INJECTION_PATTERN_COUNT = claimsRaw.evalRules.injectionPatterns;
|
|
35
|
+
export const HALLUCINATION_MARKER_COUNT = claimsRaw.evalRules.hallucinationMarkers;
|
|
36
|
+
// LLM-judge templates
|
|
37
|
+
export const LLM_JUDGE_TEMPLATE_COUNT = claimsRaw.llmJudgeTemplates.count;
|
|
38
|
+
export const LLM_JUDGE_TEMPLATE_NAMES = claimsRaw.llmJudgeTemplates.names;
|
|
39
|
+
// Brand
|
|
40
|
+
export const TAGLINE = claimsRaw.brand.tagline;
|
|
41
|
+
export const CATEGORY_NAME = claimsRaw.brand.categoryName;
|
|
42
|
+
export const COINED_TERMS = claimsRaw.brand.coinedTerms;
|
|
43
|
+
export const WEBSITE_URL = claimsRaw.brand.websiteUrl;
|
|
44
|
+
export const PUBLIC_REPO_URL = claimsRaw.brand.publicRepoUrl;
|
|
45
|
+
export const NPM_PACKAGE = claimsRaw.brand.npmPackage;
|
|
46
|
+
export const SUPPORT_EMAIL = claimsRaw.brand.supportEmail;
|
|
47
|
+
export const SECURITY_EMAIL = claimsRaw.brand.securityEmail;
|
|
48
|
+
// Release
|
|
49
|
+
export const CURRENT_RELEASE_VERSION = claimsRaw.release.currentReleaseVersion;
|
|
50
|
+
export const CURRENT_RELEASE_DATE = claimsRaw.release.currentReleaseDate;
|
|
51
|
+
export const NEXT_PLANNED_VERSION = claimsRaw.release.nextPlannedVersion;
|
|
52
|
+
export const NEXT_PLANNED_SCOPE = claimsRaw.release.nextPlannedScope;
|
package/dist/middleware/auth.js
CHANGED
|
@@ -5,6 +5,7 @@ export function createAuthMiddleware(config) {
|
|
|
5
5
|
return (_req, _res, next) => next();
|
|
6
6
|
}
|
|
7
7
|
const keyBuffer = Buffer.from(apiKey);
|
|
8
|
+
const keyLen = keyBuffer.length;
|
|
8
9
|
return (req, res, next) => {
|
|
9
10
|
if (req.path === '/health' || req.path === '/api/v1/health') {
|
|
10
11
|
return next();
|
|
@@ -14,8 +15,18 @@ export function createAuthMiddleware(config) {
|
|
|
14
15
|
res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
18
|
+
// Pad the incoming token to the configured-key length and run
|
|
19
|
+
// timingSafeEqual on same-size buffers. The byte-compare and the
|
|
20
|
+
// length-equality check are computed independently before being
|
|
21
|
+
// combined, so the request takes the same compare path regardless
|
|
22
|
+
// of whether the token's length matches — eliminating the precise
|
|
23
|
+
// length-equality fast-path the original code had.
|
|
17
24
|
const tokenBuffer = Buffer.from(authHeader.slice(7));
|
|
18
|
-
|
|
25
|
+
const candidate = Buffer.alloc(keyLen);
|
|
26
|
+
tokenBuffer.copy(candidate, 0, 0, keyLen);
|
|
27
|
+
const cmpEq = timingSafeEqual(candidate, keyBuffer);
|
|
28
|
+
const lenEq = tokenBuffer.length === keyLen;
|
|
29
|
+
if (!(cmpEq && lenEq)) {
|
|
19
30
|
res.status(403).json({ error: 'Invalid API key' });
|
|
20
31
|
return;
|
|
21
32
|
}
|
package/dist/middleware/cors.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
// Wildcard `*` in an origin pattern matches a SINGLE label only — no dots,
|
|
2
|
+
// colons, or slashes. Previously substituted `.*`, which let
|
|
3
|
+
// `http://localhost:*` match `http://localhost:8080.evil.com` (the `.*`
|
|
4
|
+
// happily consumed `8080.evil.com`). Single-label match prevents the
|
|
5
|
+
// label-crossing bypass while still supporting `*.example.com` for
|
|
6
|
+
// per-subdomain allowlists and `localhost:*` for ephemeral dev ports.
|
|
1
7
|
function isOriginAllowed(origin, allowedOrigins) {
|
|
2
8
|
for (const pattern of allowedOrigins) {
|
|
3
9
|
if (pattern === '*')
|
|
4
10
|
return true;
|
|
5
11
|
if (pattern === origin)
|
|
6
12
|
return true;
|
|
7
|
-
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '
|
|
13
|
+
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.:/]+') + '$');
|
|
8
14
|
if (regex.test(origin))
|
|
9
15
|
return true;
|
|
10
16
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { RequestHandler, Request } from 'express';
|
|
2
2
|
import type { TenantId } from '../types/tenant.js';
|
|
3
|
-
declare
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
declare global {
|
|
4
|
+
namespace Express {
|
|
5
|
+
interface Request {
|
|
6
|
+
tenantId?: TenantId;
|
|
7
|
+
}
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
/** Read `req.tenantId` with a fail-safe guarantee for downstream code. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* log row is the permanent record that the rule ever existed.
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
13
14
|
const inputSchema = {
|
|
14
15
|
rule_id: z
|
|
15
16
|
.string()
|
|
@@ -22,6 +23,8 @@ export function registerDeleteRuleTool(server, customRuleStore) {
|
|
|
22
23
|
description: [
|
|
23
24
|
'Remove a deployed custom evaluation rule. The rule stops firing on future evaluate_output calls; past eval_results that referenced it are preserved.',
|
|
24
25
|
'',
|
|
26
|
+
'Sibling tools — deploy_rule adds custom rules, list_rules enumerates them, evaluate_output runs them. delete_trace handles trace deletion (separate concern); log_trace / get_traces handle trace I/O. delete_rule is the DESTRUCTIVE remove path for the custom-rule store; it does NOT touch traces, eval_results, or built-in (non-custom) rules.',
|
|
27
|
+
'',
|
|
25
28
|
'Behavior. DESTRUCTIVE — rewrites ~/.iris/custom-rules.json without the deleted row and appends a `rule.delete` entry to the audit log (~/.iris/audit.log). Not idempotent: deleting an already-deleted rule returns `deleted: false` rather than re-emitting the audit row. The rule stops firing immediately on the live process. Historical eval_results that reference this rule_id stay in the database — drift analytics + audit trail remain valid. Tenant-scoped in Cloud tier; OSS operates on LOCAL_TENANT. Rate-limited to 20 req/min on HTTP MCP.',
|
|
26
29
|
'',
|
|
27
30
|
'Output shape. Returns JSON: `{ "deleted": boolean, "rule_id": string }`. `deleted=true` if a row was removed; `deleted=false` if no rule with that id existed.',
|
|
@@ -30,6 +33,8 @@ export function registerDeleteRuleTool(server, customRuleStore) {
|
|
|
30
33
|
'',
|
|
31
34
|
"Don't use to pause a rule (toggle in the dashboard preserves history better). Don't use on built-in (non-custom) rules — the rule_id format checks for `rule-<hex>` custom ids; built-ins aren't in the store. Don't use to delete a trace or eval result (use delete_trace for traces; eval_results deletion is not exposed in v0.4 — they fall under data retention).",
|
|
32
35
|
'',
|
|
36
|
+
'Parameters. rule_id is the only parameter; must match `rule-<lowercase-hex>` format (Zod regex). Format mismatch fails Zod with 400 BEFORE the store is touched. Cross-tenant rule_ids return `deleted: false` silently — they\'re invisible to the caller\'s tenant rather than producing a not-found error (prevents enumeration attacks). The rule_id you pass is exactly what list_rules returned in `id` or what deploy_rule returned in `rule.id`.',
|
|
37
|
+
'',
|
|
33
38
|
"Error modes. Throws 400 on malformed rule_id (wrong prefix). Returns `{deleted: false}` if rule_id doesn't match any deployed rule (not an error — idempotent-ish). Returns 429 on HTTP rate limit. File-write failures propagate as 500.",
|
|
34
39
|
].join('\n'),
|
|
35
40
|
inputSchema,
|
|
@@ -40,7 +45,8 @@ export function registerDeleteRuleTool(server, customRuleStore) {
|
|
|
40
45
|
openWorldHint: false,
|
|
41
46
|
},
|
|
42
47
|
}, async (args) => {
|
|
43
|
-
|
|
48
|
+
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
49
|
+
const deleted = customRuleStore.delete(LOCAL_TENANT, args.rule_id, 'mcp');
|
|
44
50
|
return {
|
|
45
51
|
content: [
|
|
46
52
|
{
|
|
@@ -23,6 +23,8 @@ export function registerDeleteTraceTool(server, storage) {
|
|
|
23
23
|
description: [
|
|
24
24
|
'Remove a single trace by id. Cascades to spans; eval_results keep the score history with trace_id NULLed.',
|
|
25
25
|
'',
|
|
26
|
+
'Sibling tools — log_trace creates traces, get_traces queries them, evaluate_output / evaluate_with_llm_judge / verify_citations score them. delete_rule handles custom-rule deletion (separate concern); list_rules / deploy_rule manage the custom-rule lifecycle. delete_trace is the DESTRUCTIVE single-row remove for traces; it does NOT touch eval_results (preserved for audit + drift analytics), spans cascade automatically.',
|
|
27
|
+
'',
|
|
26
28
|
'Behavior. DESTRUCTIVE — SQL DELETE scoped to the caller\'s tenant_id. Cascades: spans belonging to this trace are deleted (FK ON DELETE CASCADE); eval_results that referenced this trace have their trace_id set to NULL (FK ON DELETE SET NULL) so aggregate dashboards + historical scores remain valid even after the trace is gone. Not idempotent: deleting an already-deleted trace returns `deleted: false`. Does not emit an audit log entry in v0.4 — traces are user-scope data, not policy changes. Rate-limited to 20 req/min on HTTP MCP.',
|
|
27
29
|
'',
|
|
28
30
|
'Output shape. Returns JSON: `{ "deleted": boolean, "trace_id": string }`. `deleted=true` if a row was removed; `deleted=false` if no trace with that id existed (or it belonged to a different tenant — cross-tenant deletes silently fail).',
|
|
@@ -31,6 +33,8 @@ export function registerDeleteTraceTool(server, storage) {
|
|
|
31
33
|
'',
|
|
32
34
|
"Don't use to clean up OLD data in bulk (use retention config with --retention-days). Don't use to PAUSE a trace — traces are immutable once stored; there's nothing to pause. Don't use to delete eval_results — eval_results survive their trace's deletion intentionally (for audit + drift analysis); they're pruned only by retention.",
|
|
33
35
|
'',
|
|
36
|
+
'Parameters. trace_id is the only parameter; must match 32-char lowercase hex (Zod regex). The trace_id you pass is exactly what log_trace returned in its response, or what get_traces returned per row. Format mismatch fails Zod with 400 BEFORE the storage layer is touched. Cross-tenant trace_ids return `deleted: false` silently — they\'re invisible to the caller\'s tenant (prevents enumeration attacks; matches delete_rule\'s tenant-isolation contract).',
|
|
37
|
+
'',
|
|
34
38
|
"Error modes. Throws 400 on malformed trace_id (wrong format: not 32-char lowercase hex). Returns `{deleted: false}` when the id doesn't exist in the caller's tenant (not an error — the trace may simply have been deleted already). Returns 429 on HTTP rate limit. Storage failures propagate as 500.",
|
|
35
39
|
].join('\n'),
|
|
36
40
|
inputSchema,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* eval_type.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
14
15
|
const CustomRuleDefinitionSchema = z.object({
|
|
15
16
|
name: z.string(),
|
|
16
17
|
type: z.enum([
|
|
@@ -52,6 +53,8 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
52
53
|
description: [
|
|
53
54
|
'Deploy a new custom evaluation rule that will fire on every future evaluate_output call of its eval category.',
|
|
54
55
|
'',
|
|
56
|
+
'Sibling tools — list_rules enumerates deployed rules, delete_rule removes them, evaluate_output runs them. log_trace / get_traces / delete_trace handle the trace lifecycle separately; evaluate_with_llm_judge / verify_citations run semantic scoring (not heuristic-rule-driven). deploy_rule is the WRITE path that grows the custom-rule library.',
|
|
57
|
+
'',
|
|
55
58
|
'Behavior. Writes a row to ~/.iris/custom-rules.json (atomic write via temp file + rename) and appends a `rule.deploy` entry to the audit log (~/.iris/audit.log). The rule activates immediately for the running process and persists across restarts. Each call mints a fresh rule_id; not idempotent (deploying twice creates two rules). Tenant-scoped in Cloud tier; OSS rules are owned by LOCAL_TENANT. Rate-limited to 20 req/min on HTTP MCP.',
|
|
56
59
|
'',
|
|
57
60
|
'Output shape. Returns JSON: `{ "rule": { "id": "rule-XXXX", "name", "description", "evalType", "severity", "definition", "enabled": true, "createdAt", "updatedAt", "version": 1, "sourceMomentId?" } }`. The returned rule is the canonical persisted form; save the `id` if you plan to update or delete later.',
|
|
@@ -60,6 +63,8 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
60
63
|
'',
|
|
61
64
|
"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.",
|
|
62
65
|
'',
|
|
66
|
+
'Parameters. name is 1-120 chars (Zod-enforced min/max); 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 affects dashboard sort + audit log signal but does NOT affect scoring (scoring uses the rule\'s weight). definition.type and definition.config must match (e.g., regex_match needs config.pattern; cost_threshold needs config.max_usd; min_length needs config.min). sourceMomentId is optional but recommended (preserves workflow-inversion provenance from Make-This-A-Rule composer). Defaults: severity="medium".',
|
|
67
|
+
'',
|
|
63
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`. Throws 400 if the eval category mismatches the definition type. 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.",
|
|
64
69
|
].join('\n'),
|
|
65
70
|
inputSchema,
|
|
@@ -70,7 +75,8 @@ export function registerDeployRuleTool(server, customRuleStore) {
|
|
|
70
75
|
openWorldHint: false,
|
|
71
76
|
},
|
|
72
77
|
}, async (args) => {
|
|
73
|
-
|
|
78
|
+
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
79
|
+
const rule = customRuleStore.deploy(LOCAL_TENANT, {
|
|
74
80
|
name: args.name,
|
|
75
81
|
description: args.description,
|
|
76
82
|
evalType: args.evalType,
|
|
@@ -10,18 +10,18 @@ const CustomRuleSchema = z.object({
|
|
|
10
10
|
weight: z.number().optional(),
|
|
11
11
|
});
|
|
12
12
|
const inputSchema = {
|
|
13
|
-
output: z.string().describe('The output text to evaluate'),
|
|
14
|
-
eval_type: z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']).default('completeness').describe('
|
|
15
|
-
expected: z.string().optional().describe('Expected output for comparison'),
|
|
16
|
-
input: z.string().optional().describe('Original input for context'),
|
|
17
|
-
trace_id: z.string().optional().describe('Link evaluation to a trace'),
|
|
18
|
-
custom_rules: z.array(CustomRuleSchema).optional().describe('Custom evaluation rules'),
|
|
19
|
-
cost_usd: z.number().optional().describe('Cost
|
|
13
|
+
output: z.string().describe('The output text to evaluate (the agent\'s response that gets scored against rules)'),
|
|
14
|
+
eval_type: z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']).default('completeness').describe('Rule bundle to apply: completeness | relevance | safety | cost | custom — picks which built-in rules fire'),
|
|
15
|
+
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 (keyword overlap vs input)'),
|
|
17
|
+
trace_id: z.string().optional().describe('Link evaluation to a trace — surfaces this eval in the dashboard\'s trace drill-through'),
|
|
18
|
+
custom_rules: z.array(CustomRuleSchema).optional().describe('Custom evaluation rules — fires REGARDLESS of eval_type; pass eval_type="custom" if you want ONLY these'),
|
|
19
|
+
cost_usd: z.number().optional().describe('Cost in USD — only consulted when eval_type="cost" (compared against cost_threshold rules)'),
|
|
20
20
|
token_usage: z.object({
|
|
21
21
|
prompt_tokens: z.number().optional(),
|
|
22
22
|
completion_tokens: z.number().optional(),
|
|
23
23
|
total_tokens: z.number().optional(),
|
|
24
|
-
}).optional().describe('Token usage
|
|
24
|
+
}).optional().describe('Token usage breakdown — only consulted when eval_type="cost" (used for token-budget rules)'),
|
|
25
25
|
};
|
|
26
26
|
export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
27
27
|
server.registerTool('evaluate_output', {
|
|
@@ -29,6 +29,8 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
29
29
|
description: [
|
|
30
30
|
'Score agent output against configurable eval rules and return a 0..1 score + per-rule breakdown.',
|
|
31
31
|
'',
|
|
32
|
+
'Sibling tools — evaluate_with_llm_judge runs semantic LLM-based scoring (slower, costs money; this tool is heuristic, free, deterministic), verify_citations checks citation grounding specifically, log_trace records executions, get_traces queries them, list_rules / deploy_rule / delete_rule manage the custom-rule lifecycle. evaluate_output is the FAST PATH for length / keyword / PII / injection / cost-threshold checks where rules are sufficient.',
|
|
33
|
+
'',
|
|
32
34
|
'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
35
|
'',
|
|
34
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).',
|
|
@@ -37,6 +39,8 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
37
39
|
'',
|
|
38
40
|
'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
41
|
'',
|
|
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".',
|
|
43
|
+
'',
|
|
40
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.',
|
|
41
45
|
].join('\n'),
|
|
42
46
|
inputSchema,
|
|
@@ -59,6 +59,8 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
59
59
|
description: [
|
|
60
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
61
|
'',
|
|
62
|
+
'Sibling tools — evaluate_output runs heuristic rules (free, deterministic, ~ms latency, no API key needed); this tool runs LLM-based semantic scoring (paid, 1-10s latency, requires API key). verify_citations is a SPECIALIZED form of LLM judging that focuses on citation grounding only. log_trace / get_traces handle trace I/O; list_rules / deploy_rule / delete_rule manage heuristic-rule lifecycle. evaluate_with_llm_judge is the GENERAL semantic-scoring path.',
|
|
63
|
+
'',
|
|
62
64
|
'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
65
|
'',
|
|
64
66
|
'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}`).',
|
|
@@ -67,6 +69,8 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
67
69
|
'',
|
|
68
70
|
"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
71
|
'',
|
|
72
|
+
'Parameters. model is required (no default — pick consciously since cost varies 100x across models). provider is auto-detected from the model name; override only for ambiguous IDs. expected is REQUIRED when template="correctness" (the reference answer to compare against); ignored for other templates. source_material is REQUIRED when template="faithfulness" (the RAG sources to ground against); ignored otherwise. input is optional but improves scoring on helpfulness/safety templates (gives the judge the user prompt that produced the output). max_cost_usd defaults to env var IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or $0.25 — the worst-case cost is computed BEFORE the call (input_tokens × prompt_price + max_output_tokens × completion_price); call refused upfront if it would exceed. max_output_tokens caps the judge response (default 512, max 4096); higher = more rationale detail + more cost. temperature default 0 (deterministic). timeout_ms default 60000. trace_id optional but recommended (links eval to trace in dashboard). Defaults: temperature=0, max_output_tokens=512, max_cost_usd=$0.25, timeout_ms=60000.',
|
|
73
|
+
'',
|
|
70
74
|
'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
75
|
].join('\n'),
|
|
72
76
|
inputSchema,
|
package/dist/tools/get-traces.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
3
|
const inputSchema = {
|
|
4
|
-
agent_name: z.string().optional().describe('Filter by agent name'),
|
|
5
|
-
framework: z.string().optional().describe('Filter by framework'),
|
|
6
|
-
since: z.string().optional().describe('ISO timestamp lower bound'),
|
|
7
|
-
until: z.string().optional().describe('ISO timestamp upper bound'),
|
|
8
|
-
min_score: z.number().optional().describe('Minimum eval score filter'),
|
|
9
|
-
max_score: z.number().optional().describe('Maximum eval score filter'),
|
|
10
|
-
limit: z.number().default(50).describe('Results per page'),
|
|
11
|
-
offset: z.number().default(0).describe('
|
|
12
|
-
sort_by: z.enum(['timestamp', 'latency_ms', 'cost_usd']).default('timestamp').describe('Sort
|
|
13
|
-
sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order'),
|
|
14
|
-
include_summary: z.boolean().default(false).describe('Include dashboard summary stats'),
|
|
4
|
+
agent_name: z.string().optional().describe('Filter by agent name — exact match (no wildcards in v0.4)'),
|
|
5
|
+
framework: z.string().optional().describe('Filter by agent framework — exact match (e.g., langchain, autogen)'),
|
|
6
|
+
since: z.string().optional().describe('ISO timestamp lower bound — return traces with timestamp >= this'),
|
|
7
|
+
until: z.string().optional().describe('ISO timestamp upper bound — return traces with timestamp < this'),
|
|
8
|
+
min_score: z.number().optional().describe('Minimum eval score filter (0..1) — applied to LATEST eval per trace, not all evals'),
|
|
9
|
+
max_score: z.number().optional().describe('Maximum eval score filter (0..1) — applied to LATEST eval per trace'),
|
|
10
|
+
limit: z.number().default(50).describe('Results per page (default 50, max 1000 — values >1000 return 400)'),
|
|
11
|
+
offset: z.number().default(0).describe('Zero-based pagination offset — skip first N results'),
|
|
12
|
+
sort_by: z.enum(['timestamp', 'latency_ms', 'cost_usd']).default('timestamp').describe('Sort by timestamp | latency_ms | cost_usd (default timestamp)'),
|
|
13
|
+
sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order: asc | desc (default desc — most recent / highest first)'),
|
|
14
|
+
include_summary: z.boolean().default(false).describe('Include dashboard summary stats in same response — saves a round-trip when ingesting for dashboards'),
|
|
15
15
|
};
|
|
16
16
|
export function registerGetTracesTool(server, storage) {
|
|
17
17
|
server.registerTool('get_traces', {
|
|
@@ -19,6 +19,8 @@ export function registerGetTracesTool(server, storage) {
|
|
|
19
19
|
description: [
|
|
20
20
|
'Query stored agent-execution traces with filters, pagination, and optional dashboard summary.',
|
|
21
21
|
'',
|
|
22
|
+
'Sibling tools — log_trace creates traces, delete_trace removes a single trace, evaluate_output / evaluate_with_llm_judge / verify_citations score them, list_rules / deploy_rule / delete_rule manage the custom-rule lifecycle. get_traces is the READ path for historical agent executions — never mutates anything.',
|
|
23
|
+
'',
|
|
22
24
|
'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
25
|
'',
|
|
24
26
|
'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`.',
|
|
@@ -27,6 +29,8 @@ export function registerGetTracesTool(server, storage) {
|
|
|
27
29
|
'',
|
|
28
30
|
'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
31
|
'',
|
|
32
|
+
'Parameters. limit defaults to 50, max 1000 (anything higher returns 400). offset is zero-based pagination. min_score / max_score filter on the LATEST eval per trace, not all evals (so a trace with one failing + one passing eval may or may not match depending on which landed last). Combining since + sort_by="latency_ms" + sort_order="desc" is the canonical "find slow recent traces" query. include_summary returns dashboard-style aggregates in the SAME response (saves a round-trip; use true for dashboard ingest, false for analytics queries that don\'t need them). agent_name and framework are exact-match (no wildcards in v0.4). Defaults: limit=50, offset=0, sort_by="timestamp", sort_order="desc", include_summary=false.',
|
|
33
|
+
'',
|
|
30
34
|
'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
35
|
].join('\n'),
|
|
32
36
|
inputSchema,
|