@iris-eval/mcp-server 0.3.1 → 0.4.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 +11 -3
- package/dist/audit-log-reader.d.ts +24 -0
- package/dist/audit-log-reader.js +87 -0
- package/dist/config/defaults.js +7 -1
- package/dist/custom-rule-store.d.ts +27 -0
- package/dist/custom-rule-store.js +188 -0
- package/dist/dashboard/assets/index-BEG5FYWH.css +1 -0
- package/dist/dashboard/assets/index-D9JHfSB2.js +12 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/audit.d.ts +3 -0
- package/dist/dashboard/routes/audit.js +36 -0
- package/dist/dashboard/routes/eval-stats.js +9 -4
- package/dist/dashboard/routes/evaluations.js +3 -1
- package/dist/dashboard/routes/filters.js +5 -3
- package/dist/dashboard/routes/health.js +8 -1
- package/dist/dashboard/routes/index.d.ts +4 -0
- package/dist/dashboard/routes/index.js +4 -0
- package/dist/dashboard/routes/moments.d.ts +3 -0
- package/dist/dashboard/routes/moments.js +115 -0
- package/dist/dashboard/routes/preferences.d.ts +3 -0
- package/dist/dashboard/routes/preferences.js +52 -0
- package/dist/dashboard/routes/rules.d.ts +10 -0
- package/dist/dashboard/routes/rules.js +169 -0
- package/dist/dashboard/routes/summary.js +3 -1
- package/dist/dashboard/routes/traces.js +7 -4
- package/dist/dashboard/server.d.ts +9 -1
- package/dist/dashboard/server.js +52 -3
- package/dist/eval/citation-verify/extract.d.ts +11 -0
- package/dist/eval/citation-verify/extract.js +102 -0
- package/dist/eval/citation-verify/resolve.d.ts +26 -0
- package/dist/eval/citation-verify/resolve.js +237 -0
- package/dist/eval/citation-verify/verifier.d.ts +43 -0
- package/dist/eval/citation-verify/verifier.js +203 -0
- package/dist/eval/decision-moment.d.ts +12 -0
- package/dist/eval/decision-moment.js +181 -0
- package/dist/eval/llm-judge/client.d.ts +28 -0
- package/dist/eval/llm-judge/client.js +183 -0
- package/dist/eval/llm-judge/evaluator.d.ts +32 -0
- package/dist/eval/llm-judge/evaluator.js +138 -0
- package/dist/eval/llm-judge/pricing.d.ts +9 -0
- package/dist/eval/llm-judge/pricing.js +31 -0
- package/dist/eval/llm-judge/templates/index.d.ts +20 -0
- package/dist/eval/llm-judge/templates/index.js +170 -0
- package/dist/eval/rules/custom.js +13 -2
- package/dist/index.js +77 -14
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/tenant.d.ts +17 -0
- package/dist/middleware/tenant.js +26 -0
- package/dist/otel/exporter.d.ts +24 -0
- package/dist/otel/exporter.js +116 -0
- package/dist/otel/lazy.d.ts +5 -0
- package/dist/otel/lazy.js +31 -0
- package/dist/otel/mapper.d.ts +24 -0
- package/dist/otel/mapper.js +208 -0
- package/dist/preferences.d.ts +129 -0
- package/dist/preferences.js +152 -0
- package/dist/resources/dashboard-summary.js +3 -1
- package/dist/resources/trace-detail.js +5 -3
- package/dist/server.d.ts +3 -1
- package/dist/server.js +9 -3
- package/dist/storage/migrations/004-tenant-id.d.ts +3 -0
- package/dist/storage/migrations/004-tenant-id.js +40 -0
- package/dist/storage/migrations/index.js +2 -1
- package/dist/storage/sqlite-adapter.d.ts +17 -15
- package/dist/storage/sqlite-adapter.js +130 -79
- package/dist/tools/delete-rule.d.ts +3 -0
- package/dist/tools/delete-rule.js +53 -0
- package/dist/tools/delete-trace.d.ts +3 -0
- package/dist/tools/delete-trace.js +54 -0
- package/dist/tools/deploy-rule.d.ts +3 -0
- package/dist/tools/deploy-rule.js +91 -0
- package/dist/tools/evaluate-output.js +23 -2
- package/dist/tools/evaluate-with-llm-judge.d.ts +3 -0
- package/dist/tools/evaluate-with-llm-judge.js +147 -0
- package/dist/tools/get-traces.js +23 -3
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +13 -1
- package/dist/tools/list-rules.d.ts +3 -0
- package/dist/tools/list-rules.js +66 -0
- package/dist/tools/log-trace.js +30 -2
- package/dist/tools/verify-citations.d.ts +3 -0
- package/dist/tools/verify-citations.js +157 -0
- package/dist/types/custom-rule.d.ts +70 -0
- package/dist/types/custom-rule.js +1 -0
- package/dist/types/decision-moment.d.ts +122 -0
- package/dist/types/decision-moment.js +17 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -1
- package/dist/types/query.d.ts +25 -15
- package/dist/types/tenant.d.ts +26 -0
- package/dist/types/tenant.js +58 -0
- package/dist/utils/open-browser.d.ts +1 -0
- package/dist/utils/open-browser.js +45 -0
- package/dist/utils/validate-port-config.d.ts +2 -0
- package/dist/utils/validate-port-config.js +9 -0
- package/package.json +4 -1
- package/server.json +2 -2
- package/dist/dashboard/assets/index-CnDg6bYi.js +0 -43
- package/dist/dashboard/assets/index-ZsBou2-c.css +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
1
2
|
export function registerDashboardSummaryResource(server, storage) {
|
|
2
3
|
server.resource('dashboard-summary', 'iris://dashboard/summary', { description: 'Dashboard summary with key metrics and trends' }, async () => {
|
|
3
|
-
|
|
4
|
+
// OSS single-tenant: summary scopes to the local user.
|
|
5
|
+
const summary = await storage.getDashboardSummary(LOCAL_TENANT);
|
|
4
6
|
return {
|
|
5
7
|
contents: [
|
|
6
8
|
{
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
1
2
|
export function registerTraceDetailResource(server, storage) {
|
|
2
3
|
server.resource('trace-detail', 'iris://traces/{trace_id}', { description: 'Full trace detail with spans and evaluation results' }, async (uri) => {
|
|
3
4
|
const traceId = uri.pathname.split('/').pop();
|
|
4
|
-
|
|
5
|
+
// OSS single-tenant: MCP caller is the local user.
|
|
6
|
+
const trace = await storage.getTrace(LOCAL_TENANT, traceId);
|
|
5
7
|
if (!trace) {
|
|
6
8
|
return {
|
|
7
9
|
contents: [
|
|
@@ -13,8 +15,8 @@ export function registerTraceDetailResource(server, storage) {
|
|
|
13
15
|
],
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
|
-
const spans = await storage.getSpansByTraceId(traceId);
|
|
17
|
-
const evals = await storage.getEvalsByTraceId(traceId);
|
|
18
|
+
const spans = await storage.getSpansByTraceId(LOCAL_TENANT, traceId);
|
|
19
|
+
const evals = await storage.getEvalsByTraceId(LOCAL_TENANT, traceId);
|
|
18
20
|
return {
|
|
19
21
|
contents: [
|
|
20
22
|
{
|
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { IrisConfig } from './types/index.js';
|
|
3
3
|
import type { IStorageAdapter } from './types/query.js';
|
|
4
|
+
import type { CustomRuleStore } from './custom-rule-store.js';
|
|
4
5
|
import { EvalEngine } from './eval/engine.js';
|
|
5
6
|
export interface IrisServer {
|
|
6
7
|
mcpServer: McpServer;
|
|
7
8
|
evalEngine: EvalEngine;
|
|
9
|
+
customRuleStore: CustomRuleStore;
|
|
8
10
|
}
|
|
9
|
-
export declare function createIrisServer(config: IrisConfig, storage: IStorageAdapter): IrisServer;
|
|
11
|
+
export declare function createIrisServer(config: IrisConfig, storage: IStorageAdapter, customRuleStore?: CustomRuleStore): IrisServer;
|
package/dist/server.js
CHANGED
|
@@ -2,13 +2,19 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { EvalEngine } from './eval/engine.js';
|
|
3
3
|
import { registerAllTools } from './tools/index.js';
|
|
4
4
|
import { registerAllResources } from './resources/index.js';
|
|
5
|
-
|
|
5
|
+
import { createCustomRuleStore } from './custom-rule-store.js';
|
|
6
|
+
export function createIrisServer(config, storage, customRuleStore) {
|
|
6
7
|
const mcpServer = new McpServer({
|
|
7
8
|
name: config.server.name,
|
|
8
9
|
version: config.server.version,
|
|
9
10
|
});
|
|
10
11
|
const evalEngine = new EvalEngine(config.eval.defaultThreshold, config.eval.ruleThresholds);
|
|
11
|
-
|
|
12
|
+
// Caller can inject a shared rule store (e.g. index.ts passes the
|
|
13
|
+
// same instance the HTTP dashboard uses, so a rule deployed via MCP
|
|
14
|
+
// is immediately visible in the dashboard without a restart). If
|
|
15
|
+
// none provided, create a fresh one loading from the default path.
|
|
16
|
+
const ruleStore = customRuleStore ?? createCustomRuleStore();
|
|
17
|
+
registerAllTools(mcpServer, storage, evalEngine, ruleStore);
|
|
12
18
|
registerAllResources(mcpServer, storage);
|
|
13
|
-
return { mcpServer, evalEngine };
|
|
19
|
+
return { mcpServer, evalEngine, customRuleStore: ruleStore };
|
|
14
20
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const id = '004-tenant-id';
|
|
2
|
+
/*
|
|
3
|
+
* Migration 004 — tenant scaffolding.
|
|
4
|
+
*
|
|
5
|
+
* Adds `tenant_id TEXT NOT NULL DEFAULT 'local'` to every data table.
|
|
6
|
+
* Existing rows are backfilled to 'local' by the default. New rows
|
|
7
|
+
* (v0.4+) MUST supply a tenant_id via the storage adapter — the default
|
|
8
|
+
* only exists so the existing-row backfill is automatic.
|
|
9
|
+
*
|
|
10
|
+
* Design notes (threat model §5.1 + §5.2):
|
|
11
|
+
* - Not NULL. Never allow a row without a tenant.
|
|
12
|
+
* - DEFAULT 'local'. Backfill is automatic; existing OSS installs
|
|
13
|
+
* continue working with zero user action.
|
|
14
|
+
* - Composite indexes put tenant_id first so per-tenant queries use
|
|
15
|
+
* the leading index column. Existing single-column indexes stay
|
|
16
|
+
* for non-tenant lookups (e.g., trace_id by primary key is still
|
|
17
|
+
* fast).
|
|
18
|
+
*
|
|
19
|
+
* Forward compatibility: when Cloud goes live, the storage backend
|
|
20
|
+
* swaps from SQLite to Postgres. The tenant_id column + index
|
|
21
|
+
* conventions translate 1:1.
|
|
22
|
+
*/
|
|
23
|
+
export function up(db) {
|
|
24
|
+
db.exec(`
|
|
25
|
+
-- traces
|
|
26
|
+
ALTER TABLE traces ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_traces_tenant_timestamp ON traces(tenant_id, timestamp);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_traces_tenant_agent ON traces(tenant_id, agent_name);
|
|
29
|
+
|
|
30
|
+
-- spans
|
|
31
|
+
ALTER TABLE spans ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_spans_tenant_trace ON spans(tenant_id, trace_id);
|
|
33
|
+
|
|
34
|
+
-- eval_results
|
|
35
|
+
ALTER TABLE eval_results ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_created ON eval_results(tenant_id, created_at);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_trace ON eval_results(tenant_id, trace_id);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_type ON eval_results(tenant_id, eval_type);
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as migration001 from './001-initial-schema.js';
|
|
2
2
|
import * as migration002 from './002-eval-skip-fields.js';
|
|
3
3
|
import * as migration003 from './003-eval-passed-index.js';
|
|
4
|
-
|
|
4
|
+
import * as migration004 from './004-tenant-id.js';
|
|
5
|
+
const migrations = [migration001, migration002, migration003, migration004];
|
|
5
6
|
export function runMigrations(db) {
|
|
6
7
|
db.exec(`
|
|
7
8
|
CREATE TABLE IF NOT EXISTS _iris_migrations (
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import type { IStorageAdapter, DashboardSummary, TraceQueryOptions, TraceQueryResult, EvalStatsPeriod, EvalStats, EvalStatsTrendBucket, EvalStatsRuleBreakdown, EvalStatsFailure } from '../types/query.js';
|
|
2
2
|
import type { Trace, Span } from '../types/trace.js';
|
|
3
3
|
import type { EvalResult } from '../types/eval.js';
|
|
4
|
+
import type { TenantId } from '../types/tenant.js';
|
|
4
5
|
export declare class SqliteAdapter implements IStorageAdapter {
|
|
5
6
|
private db;
|
|
6
7
|
constructor(dbPath: string);
|
|
7
8
|
initialize(): Promise<void>;
|
|
8
9
|
close(): Promise<void>;
|
|
9
|
-
insertTrace(trace: Trace): Promise<void>;
|
|
10
|
-
getTrace(traceId: string): Promise<Trace | null>;
|
|
11
|
-
queryTraces(options: TraceQueryOptions): Promise<TraceQueryResult>;
|
|
12
|
-
insertSpan(span: Span): Promise<void>;
|
|
13
|
-
getSpansByTraceId(traceId: string): Promise<Span[]>;
|
|
14
|
-
insertEvalResult(result: EvalResult): Promise<void>;
|
|
15
|
-
getEvalsByTraceId(traceId: string): Promise<EvalResult[]>;
|
|
16
|
-
queryEvalResults(options: {
|
|
10
|
+
insertTrace(tenantId: TenantId, trace: Trace): Promise<void>;
|
|
11
|
+
getTrace(tenantId: TenantId, traceId: string): Promise<Trace | null>;
|
|
12
|
+
queryTraces(tenantId: TenantId, options: TraceQueryOptions): Promise<TraceQueryResult>;
|
|
13
|
+
insertSpan(tenantId: TenantId, span: Span): Promise<void>;
|
|
14
|
+
getSpansByTraceId(tenantId: TenantId, traceId: string): Promise<Span[]>;
|
|
15
|
+
insertEvalResult(tenantId: TenantId, result: EvalResult): Promise<void>;
|
|
16
|
+
getEvalsByTraceId(tenantId: TenantId, traceId: string): Promise<EvalResult[]>;
|
|
17
|
+
queryEvalResults(tenantId: TenantId, options: {
|
|
17
18
|
eval_type?: string;
|
|
18
19
|
passed?: boolean;
|
|
19
20
|
since?: string;
|
|
@@ -24,14 +25,15 @@ export declare class SqliteAdapter implements IStorageAdapter {
|
|
|
24
25
|
results: EvalResult[];
|
|
25
26
|
total: number;
|
|
26
27
|
}>;
|
|
27
|
-
getDashboardSummary(sinceHours?: number): Promise<DashboardSummary>;
|
|
28
|
+
getDashboardSummary(tenantId: TenantId, sinceHours?: number): Promise<DashboardSummary>;
|
|
28
29
|
private periodToSince;
|
|
29
|
-
getEvalStats(period: EvalStatsPeriod): Promise<EvalStats>;
|
|
30
|
-
getEvalStatsTrend(period: EvalStatsPeriod): Promise<EvalStatsTrendBucket[]>;
|
|
31
|
-
getEvalStatsRules(period: EvalStatsPeriod): Promise<EvalStatsRuleBreakdown[]>;
|
|
32
|
-
getEvalStatsFailures(period: EvalStatsPeriod, limit: number): Promise<EvalStatsFailure[]>;
|
|
33
|
-
deleteTracesOlderThan(days: number): Promise<number>;
|
|
34
|
-
|
|
30
|
+
getEvalStats(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStats>;
|
|
31
|
+
getEvalStatsTrend(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStatsTrendBucket[]>;
|
|
32
|
+
getEvalStatsRules(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStatsRuleBreakdown[]>;
|
|
33
|
+
getEvalStatsFailures(tenantId: TenantId, period: EvalStatsPeriod, limit: number): Promise<EvalStatsFailure[]>;
|
|
34
|
+
deleteTracesOlderThan(tenantId: TenantId, days: number): Promise<number>;
|
|
35
|
+
deleteTrace(tenantId: TenantId, traceId: string): Promise<boolean>;
|
|
36
|
+
getDistinctValues(tenantId: TenantId, column: string): Promise<string[]>;
|
|
35
37
|
private rowToTrace;
|
|
36
38
|
private rowToSpan;
|
|
37
39
|
private rowToEvalResult;
|
|
@@ -1,7 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SqliteAdapter — tenant-enforcing SQLite implementation of IStorageAdapter.
|
|
3
|
+
*
|
|
4
|
+
* Every public method takes a TenantId as its first parameter and uses
|
|
5
|
+
* it in the SQL layer to prevent cross-tenant data leaks. See the
|
|
6
|
+
* 2026-04-23 threat model §5 for the design principles.
|
|
7
|
+
*
|
|
8
|
+
* Discipline:
|
|
9
|
+
* - Every method first validates tenantId is a non-empty string.
|
|
10
|
+
* If validation fails, throws TenantContextRequiredError. This is
|
|
11
|
+
* defense-in-depth — the TenantId type system already prevents
|
|
12
|
+
* empty strings at compile time, but we verify at runtime too so
|
|
13
|
+
* any dynamic bypass (e.g. a buggy cast) still fails safe.
|
|
14
|
+
* - Every INSERT binds tenant_id from the parameter, never from the
|
|
15
|
+
* payload data.
|
|
16
|
+
* - Every SELECT includes `WHERE tenant_id = ?` as the first
|
|
17
|
+
* condition; composite indexes put tenant_id first.
|
|
18
|
+
* - Aggregate queries (stats, trends) scope to the tenant.
|
|
19
|
+
* - DELETE operations scope to the tenant — a tenant can only delete
|
|
20
|
+
* its own data.
|
|
21
|
+
*/
|
|
1
22
|
import Database from 'better-sqlite3';
|
|
23
|
+
import { TenantContextRequiredError } from '../types/tenant.js';
|
|
2
24
|
import { runMigrations } from './migrations/index.js';
|
|
3
25
|
const ALLOWED_SORT_COLUMNS = new Set(['timestamp', 'latency_ms', 'cost_usd']);
|
|
4
26
|
const ALLOWED_SORT_ORDERS = new Set(['asc', 'desc']);
|
|
27
|
+
/**
|
|
28
|
+
* Defense-in-depth runtime check. The TypeScript brand prevents most
|
|
29
|
+
* misuse at compile time; this catches any dynamic cast bypass.
|
|
30
|
+
*/
|
|
31
|
+
function assertTenant(tenantId) {
|
|
32
|
+
if (typeof tenantId !== 'string' || tenantId.length === 0) {
|
|
33
|
+
throw new TenantContextRequiredError('SqliteAdapter method invoked without a valid TenantId; refusing to query');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
5
36
|
export class SqliteAdapter {
|
|
6
37
|
db;
|
|
7
38
|
constructor(dbPath) {
|
|
@@ -16,34 +47,39 @@ export class SqliteAdapter {
|
|
|
16
47
|
async close() {
|
|
17
48
|
this.db.close();
|
|
18
49
|
}
|
|
19
|
-
async insertTrace(trace) {
|
|
50
|
+
async insertTrace(tenantId, trace) {
|
|
51
|
+
assertTenant(tenantId);
|
|
20
52
|
const insertTraceStmt = this.db.prepare(`
|
|
21
|
-
INSERT INTO traces (trace_id, agent_name, framework, input, output, tool_calls, latency_ms, token_usage, cost_usd, metadata, timestamp)
|
|
22
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
INSERT INTO traces (tenant_id, trace_id, agent_name, framework, input, output, tool_calls, latency_ms, token_usage, cost_usd, metadata, timestamp)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
23
55
|
`);
|
|
24
56
|
const insertSpanStmt = this.db.prepare(`
|
|
25
|
-
INSERT INTO spans (span_id, trace_id, parent_span_id, name, kind, status_code, status_message, start_time, end_time, attributes, events)
|
|
26
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
57
|
+
INSERT INTO spans (tenant_id, span_id, trace_id, parent_span_id, name, kind, status_code, status_message, start_time, end_time, attributes, events)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
59
|
`);
|
|
28
60
|
const insertAll = this.db.transaction((t) => {
|
|
29
|
-
insertTraceStmt.run(t.trace_id, t.agent_name, t.framework ?? null, t.input ?? null, t.output ?? null, t.tool_calls ? JSON.stringify(t.tool_calls) : null, t.latency_ms ?? null, t.token_usage ? JSON.stringify(t.token_usage) : null, t.cost_usd ?? null, t.metadata ? JSON.stringify(t.metadata) : null, t.timestamp);
|
|
61
|
+
insertTraceStmt.run(tenantId, t.trace_id, t.agent_name, t.framework ?? null, t.input ?? null, t.output ?? null, t.tool_calls ? JSON.stringify(t.tool_calls) : null, t.latency_ms ?? null, t.token_usage ? JSON.stringify(t.token_usage) : null, t.cost_usd ?? null, t.metadata ? JSON.stringify(t.metadata) : null, t.timestamp);
|
|
30
62
|
if (t.spans) {
|
|
31
63
|
for (const span of t.spans) {
|
|
32
|
-
insertSpanStmt.run(span.span_id, t.trace_id, span.parent_span_id ?? null, span.name, span.kind, span.status_code, span.status_message ?? null, span.start_time, span.end_time ?? null, span.attributes ? JSON.stringify(span.attributes) : null, span.events ? JSON.stringify(span.events) : null);
|
|
64
|
+
insertSpanStmt.run(tenantId, span.span_id, t.trace_id, span.parent_span_id ?? null, span.name, span.kind, span.status_code, span.status_message ?? null, span.start_time, span.end_time ?? null, span.attributes ? JSON.stringify(span.attributes) : null, span.events ? JSON.stringify(span.events) : null);
|
|
33
65
|
}
|
|
34
66
|
}
|
|
35
67
|
});
|
|
36
68
|
insertAll(trace);
|
|
37
69
|
}
|
|
38
|
-
async getTrace(traceId) {
|
|
39
|
-
|
|
70
|
+
async getTrace(tenantId, traceId) {
|
|
71
|
+
assertTenant(tenantId);
|
|
72
|
+
const row = this.db
|
|
73
|
+
.prepare('SELECT * FROM traces WHERE tenant_id = ? AND trace_id = ?')
|
|
74
|
+
.get(tenantId, traceId);
|
|
40
75
|
if (!row)
|
|
41
76
|
return null;
|
|
42
77
|
return this.rowToTrace(row);
|
|
43
78
|
}
|
|
44
|
-
async queryTraces(options) {
|
|
45
|
-
|
|
46
|
-
const
|
|
79
|
+
async queryTraces(tenantId, options) {
|
|
80
|
+
assertTenant(tenantId);
|
|
81
|
+
const conditions = ['tenant_id = ?'];
|
|
82
|
+
const params = [tenantId];
|
|
47
83
|
const filter = options.filter;
|
|
48
84
|
if (filter?.agent_name) {
|
|
49
85
|
conditions.push('agent_name = ?');
|
|
@@ -62,14 +98,14 @@ export class SqliteAdapter {
|
|
|
62
98
|
params.push(filter.until);
|
|
63
99
|
}
|
|
64
100
|
if (filter?.min_score !== undefined) {
|
|
65
|
-
conditions.push('EXISTS (SELECT 1 FROM eval_results e WHERE e.trace_id = traces.trace_id AND e.score >= ?)');
|
|
101
|
+
conditions.push('EXISTS (SELECT 1 FROM eval_results e WHERE e.tenant_id = traces.tenant_id AND e.trace_id = traces.trace_id AND e.score >= ?)');
|
|
66
102
|
params.push(filter.min_score);
|
|
67
103
|
}
|
|
68
104
|
if (filter?.max_score !== undefined) {
|
|
69
|
-
conditions.push('EXISTS (SELECT 1 FROM eval_results e WHERE e.trace_id = traces.trace_id AND e.score <= ?)');
|
|
105
|
+
conditions.push('EXISTS (SELECT 1 FROM eval_results e WHERE e.tenant_id = traces.tenant_id AND e.trace_id = traces.trace_id AND e.score <= ?)');
|
|
70
106
|
params.push(filter.max_score);
|
|
71
107
|
}
|
|
72
|
-
const whereClause =
|
|
108
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
73
109
|
const sortBy = options.sort_by ?? 'timestamp';
|
|
74
110
|
const sortOrder = options.sort_order ?? 'desc';
|
|
75
111
|
if (!ALLOWED_SORT_COLUMNS.has(sortBy)) {
|
|
@@ -93,33 +129,38 @@ export class SqliteAdapter {
|
|
|
93
129
|
offset,
|
|
94
130
|
};
|
|
95
131
|
}
|
|
96
|
-
async insertSpan(span) {
|
|
132
|
+
async insertSpan(tenantId, span) {
|
|
133
|
+
assertTenant(tenantId);
|
|
97
134
|
this.db.prepare(`
|
|
98
|
-
INSERT INTO spans (span_id, trace_id, parent_span_id, name, kind, status_code, status_message, start_time, end_time, attributes, events)
|
|
99
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
100
|
-
`).run(span.span_id, span.trace_id, span.parent_span_id ?? null, span.name, span.kind, span.status_code, span.status_message ?? null, span.start_time, span.end_time ?? null, span.attributes ? JSON.stringify(span.attributes) : null, span.events ? JSON.stringify(span.events) : null);
|
|
135
|
+
INSERT INTO spans (tenant_id, span_id, trace_id, parent_span_id, name, kind, status_code, status_message, start_time, end_time, attributes, events)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
|
+
`).run(tenantId, span.span_id, span.trace_id, span.parent_span_id ?? null, span.name, span.kind, span.status_code, span.status_message ?? null, span.start_time, span.end_time ?? null, span.attributes ? JSON.stringify(span.attributes) : null, span.events ? JSON.stringify(span.events) : null);
|
|
101
138
|
}
|
|
102
|
-
async getSpansByTraceId(traceId) {
|
|
139
|
+
async getSpansByTraceId(tenantId, traceId) {
|
|
140
|
+
assertTenant(tenantId);
|
|
103
141
|
const rows = this.db
|
|
104
|
-
.prepare('SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time')
|
|
105
|
-
.all(traceId);
|
|
142
|
+
.prepare('SELECT * FROM spans WHERE tenant_id = ? AND trace_id = ? ORDER BY start_time')
|
|
143
|
+
.all(tenantId, traceId);
|
|
106
144
|
return rows.map((row) => this.rowToSpan(row));
|
|
107
145
|
}
|
|
108
|
-
async insertEvalResult(result) {
|
|
146
|
+
async insertEvalResult(tenantId, result) {
|
|
147
|
+
assertTenant(tenantId);
|
|
109
148
|
this.db.prepare(`
|
|
110
|
-
INSERT INTO eval_results (id, trace_id, eval_type, output_text, expected_text, score, passed, rule_results, suggestions, rules_evaluated, rules_skipped, insufficient_data)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
112
|
-
`).run(result.id, result.trace_id ?? null, result.eval_type, result.output_text, result.expected_text ?? null, result.score, result.passed ? 1 : 0, JSON.stringify(result.rule_results), JSON.stringify(result.suggestions), result.rules_evaluated ?? null, result.rules_skipped ?? null, result.insufficient_data ? 1 : 0);
|
|
149
|
+
INSERT INTO eval_results (tenant_id, id, trace_id, eval_type, output_text, expected_text, score, passed, rule_results, suggestions, rules_evaluated, rules_skipped, insufficient_data)
|
|
150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
`).run(tenantId, result.id, result.trace_id ?? null, result.eval_type, result.output_text, result.expected_text ?? null, result.score, result.passed ? 1 : 0, JSON.stringify(result.rule_results), JSON.stringify(result.suggestions), result.rules_evaluated ?? null, result.rules_skipped ?? null, result.insufficient_data ? 1 : 0);
|
|
113
152
|
}
|
|
114
|
-
async getEvalsByTraceId(traceId) {
|
|
153
|
+
async getEvalsByTraceId(tenantId, traceId) {
|
|
154
|
+
assertTenant(tenantId);
|
|
115
155
|
const rows = this.db
|
|
116
|
-
.prepare('SELECT * FROM eval_results WHERE trace_id = ? ORDER BY created_at DESC')
|
|
117
|
-
.all(traceId);
|
|
156
|
+
.prepare('SELECT * FROM eval_results WHERE tenant_id = ? AND trace_id = ? ORDER BY created_at DESC')
|
|
157
|
+
.all(tenantId, traceId);
|
|
118
158
|
return rows.map((row) => this.rowToEvalResult(row));
|
|
119
159
|
}
|
|
120
|
-
async queryEvalResults(options) {
|
|
121
|
-
|
|
122
|
-
const
|
|
160
|
+
async queryEvalResults(tenantId, options) {
|
|
161
|
+
assertTenant(tenantId);
|
|
162
|
+
const conditions = ['tenant_id = ?'];
|
|
163
|
+
const params = [tenantId];
|
|
123
164
|
if (options.eval_type) {
|
|
124
165
|
conditions.push('eval_type = ?');
|
|
125
166
|
params.push(options.eval_type);
|
|
@@ -136,7 +177,7 @@ export class SqliteAdapter {
|
|
|
136
177
|
conditions.push('created_at <= ?');
|
|
137
178
|
params.push(options.until);
|
|
138
179
|
}
|
|
139
|
-
const whereClause =
|
|
180
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
140
181
|
const limit = options.limit ?? 50;
|
|
141
182
|
const offset = options.offset ?? 0;
|
|
142
183
|
const countRow = this.db
|
|
@@ -150,37 +191,38 @@ export class SqliteAdapter {
|
|
|
150
191
|
total: countRow.count,
|
|
151
192
|
};
|
|
152
193
|
}
|
|
153
|
-
async getDashboardSummary(sinceHours = 24) {
|
|
194
|
+
async getDashboardSummary(tenantId, sinceHours = 24) {
|
|
195
|
+
assertTenant(tenantId);
|
|
154
196
|
const since = new Date(Date.now() - sinceHours * 60 * 60 * 1000).toISOString();
|
|
155
197
|
const stats = this.db.prepare(`
|
|
156
198
|
SELECT
|
|
157
199
|
COUNT(*) as total_traces,
|
|
158
200
|
COALESCE(AVG(latency_ms), 0) as avg_latency_ms,
|
|
159
201
|
COALESCE(SUM(cost_usd), 0) as total_cost_usd
|
|
160
|
-
FROM traces WHERE timestamp >= ?
|
|
161
|
-
`).get(since);
|
|
202
|
+
FROM traces WHERE tenant_id = ? AND timestamp >= ?
|
|
203
|
+
`).get(tenantId, since);
|
|
162
204
|
const errorCount = this.db.prepare(`
|
|
163
205
|
SELECT COUNT(DISTINCT t.trace_id) as count
|
|
164
206
|
FROM traces t
|
|
165
|
-
JOIN spans s ON s.trace_id = t.trace_id
|
|
166
|
-
WHERE t.timestamp >= ? AND s.status_code = 'ERROR'
|
|
167
|
-
`).get(since);
|
|
207
|
+
JOIN spans s ON s.tenant_id = t.tenant_id AND s.trace_id = t.trace_id
|
|
208
|
+
WHERE t.tenant_id = ? AND t.timestamp >= ? AND s.status_code = 'ERROR'
|
|
209
|
+
`).get(tenantId, since);
|
|
168
210
|
const evalStats = this.db.prepare(`
|
|
169
211
|
SELECT
|
|
170
212
|
COUNT(*) as total,
|
|
171
213
|
SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) as passed_count
|
|
172
|
-
FROM eval_results WHERE created_at >= ?
|
|
173
|
-
`).get(since);
|
|
214
|
+
FROM eval_results WHERE tenant_id = ? AND created_at >= ?
|
|
215
|
+
`).get(tenantId, since);
|
|
174
216
|
const tracesPerHour = this.db.prepare(`
|
|
175
217
|
SELECT strftime('%Y-%m-%dT%H:00:00', timestamp) as hour, COUNT(*) as count
|
|
176
|
-
FROM traces WHERE timestamp >= ?
|
|
218
|
+
FROM traces WHERE tenant_id = ? AND timestamp >= ?
|
|
177
219
|
GROUP BY hour ORDER BY hour
|
|
178
|
-
`).all(since);
|
|
220
|
+
`).all(tenantId, since);
|
|
179
221
|
const topAgents = this.db.prepare(`
|
|
180
222
|
SELECT agent_name, COUNT(*) as count
|
|
181
|
-
FROM traces WHERE timestamp >= ?
|
|
223
|
+
FROM traces WHERE tenant_id = ? AND timestamp >= ?
|
|
182
224
|
GROUP BY agent_name ORDER BY count DESC LIMIT 10
|
|
183
|
-
`).all(since);
|
|
225
|
+
`).all(tenantId, since);
|
|
184
226
|
return {
|
|
185
227
|
total_traces: stats.total_traces,
|
|
186
228
|
avg_latency_ms: Math.round(stats.avg_latency_ms * 100) / 100,
|
|
@@ -200,7 +242,8 @@ export class SqliteAdapter {
|
|
|
200
242
|
const hours = period === '24h' ? 24 : period === '7d' ? 168 : 720;
|
|
201
243
|
return new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
202
244
|
}
|
|
203
|
-
async getEvalStats(period) {
|
|
245
|
+
async getEvalStats(tenantId, period) {
|
|
246
|
+
assertTenant(tenantId);
|
|
204
247
|
const since = this.periodToSince(period);
|
|
205
248
|
const agg = this.db.prepare(`
|
|
206
249
|
SELECT
|
|
@@ -208,27 +251,25 @@ export class SqliteAdapter {
|
|
|
208
251
|
COALESCE(AVG(score), 0) AS avg_score,
|
|
209
252
|
SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) AS passed_count
|
|
210
253
|
FROM eval_results
|
|
211
|
-
WHERE created_at >= ? AND trace_id IS NOT NULL
|
|
212
|
-
`).get(since);
|
|
254
|
+
WHERE tenant_id = ? AND created_at >= ? AND trace_id IS NOT NULL
|
|
255
|
+
`).get(tenantId, since);
|
|
213
256
|
const cost = this.db.prepare(`
|
|
214
257
|
SELECT COALESCE(SUM(cost_usd), 0) AS total_cost
|
|
215
258
|
FROM traces
|
|
216
|
-
WHERE timestamp >= ?
|
|
217
|
-
`).get(since);
|
|
259
|
+
WHERE tenant_id = ? AND timestamp >= ?
|
|
260
|
+
`).get(tenantId, since);
|
|
218
261
|
const agents = this.db.prepare(`
|
|
219
262
|
SELECT COUNT(DISTINCT agent_name) AS agent_count
|
|
220
263
|
FROM traces
|
|
221
|
-
WHERE timestamp >= ?
|
|
222
|
-
`).get(since);
|
|
223
|
-
// Safety violations — scan rule_results JSON for failing safety rules.
|
|
224
|
-
// rule_results is stored as a JSON array of EvalRuleResult objects.
|
|
264
|
+
WHERE tenant_id = ? AND timestamp >= ?
|
|
265
|
+
`).get(tenantId, since);
|
|
225
266
|
const safetyRows = this.db.prepare(`
|
|
226
267
|
SELECT rule_results
|
|
227
268
|
FROM eval_results
|
|
228
|
-
WHERE created_at >= ?
|
|
269
|
+
WHERE tenant_id = ? AND created_at >= ?
|
|
229
270
|
AND eval_type = 'safety'
|
|
230
271
|
AND passed = 0
|
|
231
|
-
`).all(since);
|
|
272
|
+
`).all(tenantId, since);
|
|
232
273
|
const violations = { pii: 0, injection: 0, hallucination: 0 };
|
|
233
274
|
for (const row of safetyRows) {
|
|
234
275
|
const rules = JSON.parse(row.rule_results);
|
|
@@ -255,21 +296,18 @@ export class SqliteAdapter {
|
|
|
255
296
|
period,
|
|
256
297
|
};
|
|
257
298
|
}
|
|
258
|
-
async getEvalStatsTrend(period) {
|
|
299
|
+
async getEvalStatsTrend(tenantId, period) {
|
|
300
|
+
assertTenant(tenantId);
|
|
259
301
|
const since = this.periodToSince(period);
|
|
260
|
-
// Determine bucket format for strftime
|
|
261
302
|
let bucketExpr;
|
|
262
303
|
if (period === '24h') {
|
|
263
|
-
// hourly buckets
|
|
264
304
|
bucketExpr = "strftime('%Y-%m-%dT%H:00:00Z', created_at)";
|
|
265
305
|
}
|
|
266
306
|
else if (period === '7d') {
|
|
267
|
-
// 6-hour buckets: floor hour to nearest 6
|
|
268
307
|
bucketExpr =
|
|
269
308
|
"strftime('%Y-%m-%dT', created_at) || printf('%02d', (CAST(strftime('%H', created_at) AS INTEGER) / 6) * 6) || ':00:00Z'";
|
|
270
309
|
}
|
|
271
310
|
else {
|
|
272
|
-
// daily buckets
|
|
273
311
|
bucketExpr = "strftime('%Y-%m-%dT00:00:00Z', created_at)";
|
|
274
312
|
}
|
|
275
313
|
const rows = this.db.prepare(`
|
|
@@ -281,10 +319,10 @@ export class SqliteAdapter {
|
|
|
281
319
|
ELSE 0 END AS pass_rate,
|
|
282
320
|
COUNT(*) AS eval_count
|
|
283
321
|
FROM eval_results
|
|
284
|
-
WHERE created_at >= ?
|
|
322
|
+
WHERE tenant_id = ? AND created_at >= ?
|
|
285
323
|
GROUP BY bucket
|
|
286
324
|
ORDER BY bucket
|
|
287
|
-
`).all(since);
|
|
325
|
+
`).all(tenantId, since);
|
|
288
326
|
return rows.map((r) => ({
|
|
289
327
|
timestamp: r.bucket,
|
|
290
328
|
avgScore: Math.round(r.avg_score * 1000) / 1000,
|
|
@@ -292,14 +330,14 @@ export class SqliteAdapter {
|
|
|
292
330
|
evalCount: r.eval_count,
|
|
293
331
|
}));
|
|
294
332
|
}
|
|
295
|
-
async getEvalStatsRules(period) {
|
|
333
|
+
async getEvalStatsRules(tenantId, period) {
|
|
334
|
+
assertTenant(tenantId);
|
|
296
335
|
const since = this.periodToSince(period);
|
|
297
336
|
const rows = this.db.prepare(`
|
|
298
337
|
SELECT rule_results
|
|
299
338
|
FROM eval_results
|
|
300
|
-
WHERE created_at >= ?
|
|
301
|
-
`).all(since);
|
|
302
|
-
// Aggregate per-rule stats from the JSON arrays
|
|
339
|
+
WHERE tenant_id = ? AND created_at >= ?
|
|
340
|
+
`).all(tenantId, since);
|
|
303
341
|
const ruleMap = new Map();
|
|
304
342
|
for (const row of rows) {
|
|
305
343
|
const rules = JSON.parse(row.rule_results);
|
|
@@ -324,11 +362,11 @@ export class SqliteAdapter {
|
|
|
324
362
|
failCount: stats.failCount,
|
|
325
363
|
});
|
|
326
364
|
}
|
|
327
|
-
// Sort by passRate ASC (worst rules first)
|
|
328
365
|
result.sort((a, b) => a.passRate - b.passRate);
|
|
329
366
|
return result;
|
|
330
367
|
}
|
|
331
|
-
async getEvalStatsFailures(period, limit) {
|
|
368
|
+
async getEvalStatsFailures(tenantId, period, limit) {
|
|
369
|
+
assertTenant(tenantId);
|
|
332
370
|
const since = this.periodToSince(period);
|
|
333
371
|
const rows = this.db.prepare(`
|
|
334
372
|
SELECT
|
|
@@ -339,14 +377,13 @@ export class SqliteAdapter {
|
|
|
339
377
|
e.output_text,
|
|
340
378
|
e.created_at
|
|
341
379
|
FROM eval_results e
|
|
342
|
-
LEFT JOIN traces t ON t.trace_id = e.trace_id
|
|
343
|
-
WHERE e.created_at >= ?
|
|
380
|
+
LEFT JOIN traces t ON t.tenant_id = e.tenant_id AND t.trace_id = e.trace_id
|
|
381
|
+
WHERE e.tenant_id = ? AND e.created_at >= ?
|
|
344
382
|
AND e.passed = 0
|
|
345
383
|
ORDER BY e.created_at DESC
|
|
346
384
|
LIMIT ?
|
|
347
|
-
`).all(since, limit);
|
|
385
|
+
`).all(tenantId, since, limit);
|
|
348
386
|
return rows.map((r) => {
|
|
349
|
-
// Find the first failing rule to surface as the primary rule
|
|
350
387
|
const rules = JSON.parse(r.rule_results);
|
|
351
388
|
const failingRule = rules.find((rule) => !rule.passed);
|
|
352
389
|
return {
|
|
@@ -359,21 +396,35 @@ export class SqliteAdapter {
|
|
|
359
396
|
};
|
|
360
397
|
});
|
|
361
398
|
}
|
|
362
|
-
async deleteTracesOlderThan(days) {
|
|
399
|
+
async deleteTracesOlderThan(tenantId, days) {
|
|
400
|
+
assertTenant(tenantId);
|
|
363
401
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
364
|
-
const result = this.db
|
|
402
|
+
const result = this.db
|
|
403
|
+
.prepare('DELETE FROM traces WHERE tenant_id = ? AND timestamp < ?')
|
|
404
|
+
.run(tenantId, cutoff);
|
|
365
405
|
return result.changes;
|
|
366
406
|
}
|
|
367
|
-
async
|
|
407
|
+
async deleteTrace(tenantId, traceId) {
|
|
408
|
+
assertTenant(tenantId);
|
|
409
|
+
// Tenant-scoped: a trace id owned by a different tenant is
|
|
410
|
+
// untouchable from this call. Cross-tenant deletions are not just
|
|
411
|
+
// denied — they're invisible (no indication the id even exists).
|
|
412
|
+
const result = this.db
|
|
413
|
+
.prepare('DELETE FROM traces WHERE tenant_id = ? AND trace_id = ?')
|
|
414
|
+
.run(tenantId, traceId);
|
|
415
|
+
return result.changes > 0;
|
|
416
|
+
}
|
|
417
|
+
async getDistinctValues(tenantId, column) {
|
|
418
|
+
assertTenant(tenantId);
|
|
368
419
|
const queries = {
|
|
369
|
-
agent_name: 'SELECT DISTINCT agent_name FROM traces WHERE agent_name IS NOT NULL ORDER BY agent_name',
|
|
370
|
-
framework: 'SELECT DISTINCT framework FROM traces WHERE framework IS NOT NULL ORDER BY framework',
|
|
420
|
+
agent_name: 'SELECT DISTINCT agent_name FROM traces WHERE tenant_id = ? AND agent_name IS NOT NULL ORDER BY agent_name',
|
|
421
|
+
framework: 'SELECT DISTINCT framework FROM traces WHERE tenant_id = ? AND framework IS NOT NULL ORDER BY framework',
|
|
371
422
|
};
|
|
372
423
|
const query = queries[column];
|
|
373
424
|
if (!query) {
|
|
374
425
|
throw new Error(`Column '${column}' is not queryable (allowed: ${Object.keys(queries).join(', ')})`);
|
|
375
426
|
}
|
|
376
|
-
const rows = this.db.prepare(query).all();
|
|
427
|
+
const rows = this.db.prepare(query).all(tenantId);
|
|
377
428
|
return rows.map((row) => row[column]);
|
|
378
429
|
}
|
|
379
430
|
rowToTrace(row) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* delete_rule MCP tool — remove a deployed custom rule.
|
|
3
|
+
*
|
|
4
|
+
* Destructive counterpart to deploy_rule. Removes the rule from
|
|
5
|
+
* ~/.iris/custom-rules.json (stops firing on future evaluate_output
|
|
6
|
+
* calls) and appends a `rule.delete` entry to the audit log.
|
|
7
|
+
*
|
|
8
|
+
* Past eval_results that referenced this rule stay intact — the
|
|
9
|
+
* history is preserved even after the rule is removed. The audit
|
|
10
|
+
* log row is the permanent record that the rule ever existed.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
const inputSchema = {
|
|
14
|
+
rule_id: z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^rule-[a-z0-9]+$/)
|
|
17
|
+
.describe('Rule id to delete (format: rule-<hex>); obtained from list_rules or deploy_rule response'),
|
|
18
|
+
};
|
|
19
|
+
export function registerDeleteRuleTool(server, customRuleStore) {
|
|
20
|
+
server.registerTool('delete_rule', {
|
|
21
|
+
title: 'Delete Custom Rule',
|
|
22
|
+
description: [
|
|
23
|
+
'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
|
+
'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
|
+
'',
|
|
27
|
+
'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.',
|
|
28
|
+
'',
|
|
29
|
+
"Use when a custom rule is obsolete (behavior changed, false positives unacceptable, replaced by a better rule). Typical flow: list_rules → identify the stale one → delete_rule(id). Combine with deploy_rule to replace: delete_rule(oldId) + deploy_rule(newDefinition). To temporarily disable a rule WITHOUT deletion, use the dashboard's toggle affordance instead — delete is permanent in intent (rule is gone; re-adding requires a new id).",
|
|
30
|
+
'',
|
|
31
|
+
"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
|
+
'',
|
|
33
|
+
"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
|
+
].join('\n'),
|
|
35
|
+
inputSchema,
|
|
36
|
+
annotations: {
|
|
37
|
+
readOnlyHint: false,
|
|
38
|
+
destructiveHint: true,
|
|
39
|
+
idempotentHint: false,
|
|
40
|
+
openWorldHint: false,
|
|
41
|
+
},
|
|
42
|
+
}, async (args) => {
|
|
43
|
+
const deleted = customRuleStore.delete(args.rule_id, 'mcp');
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: JSON.stringify({ deleted, rule_id: args.rule_id }),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|