@iris-eval/mcp-server 0.4.0 → 0.4.2
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/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
package/README.md
CHANGED
|
@@ -126,8 +126,8 @@ Iris registers nine tools that any MCP-compatible agent can invoke — full rule
|
|
|
126
126
|
- **`deploy_rule`** — Register a new custom eval rule so it fires on every `evaluate_output` of that category
|
|
127
127
|
- **`delete_rule`** — Remove a deployed custom rule (destructive, idempotent)
|
|
128
128
|
- **`delete_trace`** — Remove a single stored trace by ID (destructive, tenant-scoped)
|
|
129
|
-
- **`evaluate_with_llm_judge`** — Semantic eval via LLM (Anthropic or OpenAI). Five templates: accuracy, helpfulness, safety, correctness, faithfulness. Cost-capped, per-eval pricing disclosed.
|
|
130
|
-
- **`verify_citations`** — Extract citations from output (numbered, author-year, URLs, DOIs), fetch sources behind an SSRF-guarded + domain-allowlisted resolver, and use an LLM judge to check whether each source actually supports the cited claim. Opt-in outbound HTTP.
|
|
129
|
+
- **`evaluate_with_llm_judge`** — Semantic eval via LLM (Anthropic or OpenAI). Five templates: accuracy, helpfulness, safety, correctness, faithfulness. Cost-capped, per-eval pricing disclosed. **Bring your own API key** (`IRIS_ANTHROPIC_API_KEY` or `IRIS_OPENAI_API_KEY`) — Iris doesn't proxy or relay LLM calls.
|
|
130
|
+
- **`verify_citations`** — Extract citations from output (numbered, author-year, URLs, DOIs), fetch sources behind an SSRF-guarded + domain-allowlisted resolver, and use an LLM judge to check whether each source actually supports the cited claim. Opt-in outbound HTTP. Same BYOK requirement as `evaluate_with_llm_judge`.
|
|
131
131
|
|
|
132
132
|
When `IRIS_OTEL_ENDPOINT` is configured, `log_trace` calls also emit a best-effort OTLP/HTTP JSON export to any OpenTelemetry collector (Jaeger, Grafana Tempo, Datadog OTLP, Honeycomb, etc). See [docs/otel-integration.md](docs/otel-integration.md).
|
|
133
133
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { DeployedCustomRule, RuleSeverity } from './types/custom-rule.js';
|
|
2
2
|
import type { CustomRuleDefinition, EvalType } from './types/eval.js';
|
|
3
|
+
import { type TenantId } from './types/tenant.js';
|
|
3
4
|
export interface CustomRuleStore {
|
|
4
|
-
list(): DeployedCustomRule[];
|
|
5
|
-
get(id: string): DeployedCustomRule | undefined;
|
|
6
|
-
deploy(input: DeployRuleInput): DeployedCustomRule;
|
|
7
|
-
delete(id: string, user?: string): boolean;
|
|
8
|
-
setEnabled(id: string, enabled: boolean, user?: string): DeployedCustomRule | undefined;
|
|
9
|
-
/** All ENABLED rules in deploy order — what the engine should register. */
|
|
10
|
-
enabledRules(): DeployedCustomRule[];
|
|
11
|
-
/** Path on disk for diagnostics. */
|
|
12
|
-
|
|
5
|
+
list(tenantId: TenantId): DeployedCustomRule[];
|
|
6
|
+
get(tenantId: TenantId, id: string): DeployedCustomRule | undefined;
|
|
7
|
+
deploy(tenantId: TenantId, input: DeployRuleInput): DeployedCustomRule;
|
|
8
|
+
delete(tenantId: TenantId, id: string, user?: string): boolean;
|
|
9
|
+
setEnabled(tenantId: TenantId, id: string, enabled: boolean, user?: string): DeployedCustomRule | undefined;
|
|
10
|
+
/** All ENABLED rules for a tenant in deploy order — what the engine should register. */
|
|
11
|
+
enabledRules(tenantId: TenantId): DeployedCustomRule[];
|
|
12
|
+
/** Path on disk for diagnostics. Different per tenant. */
|
|
13
|
+
pathFor(tenantId: TenantId): string;
|
|
13
14
|
auditPath: string;
|
|
14
15
|
}
|
|
15
16
|
export interface DeployRuleInput {
|
|
@@ -22,6 +23,13 @@ export interface DeployRuleInput {
|
|
|
22
23
|
user?: string;
|
|
23
24
|
}
|
|
24
25
|
export declare function createCustomRuleStore(opts?: {
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Returns the file path for a tenant's rules. Defaults to
|
|
28
|
+
* `~/.iris/custom-rules.json` for LOCAL_TENANT (zero migration for OSS)
|
|
29
|
+
* and `~/.iris/custom-rules-<sanitized-tenantId>.json` for others.
|
|
30
|
+
* Cloud orchestrators can inject their own factory to e.g. write into
|
|
31
|
+
* a per-tenant data dir.
|
|
32
|
+
*/
|
|
33
|
+
pathFor?: (tenantId: TenantId) => string;
|
|
26
34
|
auditPath?: string;
|
|
27
35
|
}): CustomRuleStore;
|
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* custom-rule-store — file-based persistence for deployed custom rules.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Per-tenant file partition: each tenant's rules live in their own
|
|
5
|
+
* file. OSS single-tenant installs continue to use
|
|
6
|
+
* ~/.iris/custom-rules.json (the LOCAL_TENANT path) — zero migration
|
|
7
|
+
* for existing users. Cloud tenants get
|
|
8
|
+
* ~/.iris/custom-rules-<tenantId>.json (or whatever path the
|
|
9
|
+
* `pathFor` factory returns).
|
|
10
|
+
*
|
|
11
|
+
* Why per-file rather than top-level keys in one file or a tenant
|
|
12
|
+
* column on each rule:
|
|
13
|
+
* - Zero migration: LOCAL_TENANT keeps the v0.4 file path/schema.
|
|
14
|
+
* - Smallest blast radius for a corrupt write: one tenant's data
|
|
15
|
+
* can't poison another's.
|
|
16
|
+
* - Mirrors the existing audit-log per-file convention.
|
|
17
|
+
*
|
|
18
|
+
* Audit log stays SHARED across tenants: every entry already carries
|
|
19
|
+
* `tenantId` so readers can scope at query time.
|
|
7
20
|
*
|
|
8
21
|
* The v0.4 cut is single-user local. Concurrent writes from multiple
|
|
9
|
-
* iris-mcp instances
|
|
10
|
-
*
|
|
11
|
-
*
|
|
22
|
+
* iris-mcp instances against the same tenant file are not protected.
|
|
23
|
+
* For now we use atomic write-via-rename so a crashed write doesn't
|
|
24
|
+
* leave a half-file.
|
|
12
25
|
*/
|
|
13
26
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync, appendFileSync } from 'node:fs';
|
|
14
27
|
import { join, dirname } from 'node:path';
|
|
15
28
|
import { homedir } from 'node:os';
|
|
16
29
|
import { randomBytes } from 'node:crypto';
|
|
17
30
|
import { z } from 'zod';
|
|
31
|
+
import { LOCAL_TENANT } from './types/tenant.js';
|
|
18
32
|
const SEVERITY_VALUES = ['low', 'medium', 'high', 'critical'];
|
|
19
33
|
const EVAL_TYPE_VALUES = ['completeness', 'relevance', 'safety', 'cost', 'custom'];
|
|
20
34
|
const RULE_TYPE_VALUES = [
|
|
@@ -50,8 +64,19 @@ const FileSchema = z.object({
|
|
|
50
64
|
version: z.literal(1),
|
|
51
65
|
rules: z.array(DeployedRuleSchema),
|
|
52
66
|
});
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Default file path for a tenant. LOCAL_TENANT keeps the v0.4 path
|
|
69
|
+
* (zero migration); others get a per-tenant suffix.
|
|
70
|
+
*/
|
|
71
|
+
function defaultPathFor(tenantId) {
|
|
72
|
+
if (tenantId === LOCAL_TENANT) {
|
|
73
|
+
return join(homedir(), '.iris', 'custom-rules.json');
|
|
74
|
+
}
|
|
75
|
+
// Sanitize tenant id for filesystem safety. TenantId is branded but
|
|
76
|
+
// could in principle contain odd chars on Cloud — limit to a known-safe
|
|
77
|
+
// alphabet so we never write outside the .iris directory.
|
|
78
|
+
const safe = String(tenantId).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
79
|
+
return join(homedir(), '.iris', `custom-rules-${safe}.json`);
|
|
55
80
|
}
|
|
56
81
|
function defaultAuditPath() {
|
|
57
82
|
return join(homedir(), '.iris', 'audit.log');
|
|
@@ -75,42 +100,54 @@ function writeAtomic(targetPath, contents) {
|
|
|
75
100
|
writeFileSync(tmp, contents, 'utf-8');
|
|
76
101
|
renameSync(tmp, targetPath);
|
|
77
102
|
}
|
|
103
|
+
function loadRulesFromDisk(rulesPath) {
|
|
104
|
+
if (!existsSync(rulesPath))
|
|
105
|
+
return [];
|
|
106
|
+
try {
|
|
107
|
+
const raw = readFileSync(rulesPath, 'utf-8');
|
|
108
|
+
const parsed = FileSchema.safeParse(JSON.parse(raw));
|
|
109
|
+
if (parsed.success)
|
|
110
|
+
return parsed.data.rules;
|
|
111
|
+
// Malformed: leave rules empty; do NOT overwrite the file.
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Unreadable: leave rules empty.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
78
119
|
export function createCustomRuleStore(opts) {
|
|
79
|
-
const
|
|
120
|
+
const pathFor = opts?.pathFor ?? defaultPathFor;
|
|
80
121
|
const auditPath = opts?.auditPath ?? defaultAuditPath();
|
|
81
|
-
// In-memory
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
rules = parsed.data.rules;
|
|
90
|
-
}
|
|
91
|
-
// Malformed: leave rules empty; do NOT overwrite the file.
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Unreadable: leave rules empty.
|
|
122
|
+
// In-memory cache keyed by tenant. Lazy-loaded on first access per
|
|
123
|
+
// tenant; subsequent calls hit the cache.
|
|
124
|
+
const tenantRules = new Map();
|
|
125
|
+
function load(tenantId) {
|
|
126
|
+
let rules = tenantRules.get(tenantId);
|
|
127
|
+
if (rules === undefined) {
|
|
128
|
+
rules = loadRulesFromDisk(pathFor(tenantId));
|
|
129
|
+
tenantRules.set(tenantId, rules);
|
|
95
130
|
}
|
|
131
|
+
return rules;
|
|
96
132
|
}
|
|
97
|
-
function persist() {
|
|
133
|
+
function persist(tenantId) {
|
|
134
|
+
const rules = tenantRules.get(tenantId) ?? [];
|
|
98
135
|
const file = { version: 1, rules };
|
|
99
|
-
writeAtomic(
|
|
136
|
+
writeAtomic(pathFor(tenantId), JSON.stringify(file, null, 2));
|
|
100
137
|
}
|
|
101
138
|
return {
|
|
102
|
-
filePath: rulesPath,
|
|
103
139
|
auditPath,
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
pathFor,
|
|
141
|
+
list(tenantId) {
|
|
142
|
+
return [...load(tenantId)];
|
|
106
143
|
},
|
|
107
|
-
get(id) {
|
|
108
|
-
return
|
|
144
|
+
get(tenantId, id) {
|
|
145
|
+
return load(tenantId).find((r) => r.id === id);
|
|
109
146
|
},
|
|
110
|
-
enabledRules() {
|
|
111
|
-
return
|
|
147
|
+
enabledRules(tenantId) {
|
|
148
|
+
return load(tenantId).filter((r) => r.enabled);
|
|
112
149
|
},
|
|
113
|
-
deploy(input) {
|
|
150
|
+
deploy(tenantId, input) {
|
|
114
151
|
const now = new Date().toISOString();
|
|
115
152
|
const id = generateRuleId();
|
|
116
153
|
const rule = {
|
|
@@ -128,15 +165,12 @@ export function createCustomRuleStore(opts) {
|
|
|
128
165
|
};
|
|
129
166
|
// Validate before persisting.
|
|
130
167
|
const validated = DeployedRuleSchema.parse(rule);
|
|
168
|
+
const rules = load(tenantId);
|
|
131
169
|
rules.push(validated);
|
|
132
|
-
persist();
|
|
133
|
-
/* Tenant scoping: OSS single-tenant emits 'local'. Cloud replaces
|
|
134
|
-
* the entire custom-rule-store with a DB-backed service that reads
|
|
135
|
-
* the tenant from the authenticated session; that service will
|
|
136
|
-
* compute tenantId per-call. Hard-coded here for OSS. */
|
|
170
|
+
persist(tenantId);
|
|
137
171
|
appendAudit(auditPath, {
|
|
138
172
|
ts: now,
|
|
139
|
-
tenantId
|
|
173
|
+
tenantId,
|
|
140
174
|
action: 'rule.deploy',
|
|
141
175
|
user: input.user ?? 'local',
|
|
142
176
|
ruleId: id,
|
|
@@ -147,16 +181,17 @@ export function createCustomRuleStore(opts) {
|
|
|
147
181
|
});
|
|
148
182
|
return validated;
|
|
149
183
|
},
|
|
150
|
-
delete(id, user = 'local') {
|
|
184
|
+
delete(tenantId, id, user = 'local') {
|
|
185
|
+
const rules = load(tenantId);
|
|
151
186
|
const idx = rules.findIndex((r) => r.id === id);
|
|
152
187
|
if (idx === -1)
|
|
153
188
|
return false;
|
|
154
189
|
const removed = rules[idx];
|
|
155
190
|
rules.splice(idx, 1);
|
|
156
|
-
persist();
|
|
191
|
+
persist(tenantId);
|
|
157
192
|
appendAudit(auditPath, {
|
|
158
193
|
ts: new Date().toISOString(),
|
|
159
|
-
tenantId
|
|
194
|
+
tenantId,
|
|
160
195
|
action: 'rule.delete',
|
|
161
196
|
user,
|
|
162
197
|
ruleId: id,
|
|
@@ -164,7 +199,8 @@ export function createCustomRuleStore(opts) {
|
|
|
164
199
|
});
|
|
165
200
|
return true;
|
|
166
201
|
},
|
|
167
|
-
setEnabled(id, enabled, user = 'local') {
|
|
202
|
+
setEnabled(tenantId, id, enabled, user = 'local') {
|
|
203
|
+
const rules = load(tenantId);
|
|
168
204
|
const rule = rules.find((r) => r.id === id);
|
|
169
205
|
if (!rule)
|
|
170
206
|
return undefined;
|
|
@@ -172,10 +208,10 @@ export function createCustomRuleStore(opts) {
|
|
|
172
208
|
return rule;
|
|
173
209
|
rule.enabled = enabled;
|
|
174
210
|
rule.updatedAt = new Date().toISOString();
|
|
175
|
-
persist();
|
|
211
|
+
persist(tenantId);
|
|
176
212
|
appendAudit(auditPath, {
|
|
177
213
|
ts: rule.updatedAt,
|
|
178
|
-
tenantId
|
|
214
|
+
tenantId,
|
|
179
215
|
action: 'rule.toggle',
|
|
180
216
|
user,
|
|
181
217
|
ruleId: id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@import "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500..700&family=Manrope:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap";:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--iris-50:#f0fdfa;--iris-100:#ccfbf1;--iris-200:#99f6e4;--iris-300:#5eead4;--iris-400:#2dd4bf;--iris-500:#14b8a6;--iris-600:#0d9488;--iris-700:#0f766e;--iris-800:#115e59;--iris-900:#134e4a;--iris-950:#042f2e;--eval-pass:#22c55e;--eval-warn:#eab308;--eval-fail:#ef4444;--eval-tool:#3b82f6;--eval-llm:#a855f7;--eval-skipped:#71717a}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root,[data-theme=dark]{--bg-base:#050508;--bg-raised:#08080e;--bg-surface:#0d0d15;--bg-card:#101018;--bg-card-hover:#16161f;--border-subtle:#ffffff0d;--border-default:#ffffff14;--border-strong:#ffffff24;--border-glow:#14b8a680;--text-primary:#f0f0f5;--text-secondary:#9494a8;--text-muted:#5e5e72;--text-accent:var(--iris-400);--glow-primary:#14b8a61f;--glow-strong:#14b8a640;--shadow-sm:0 1px 2px #0000004d;--shadow-md:0 4px 6px #0006;--shadow-lg:0 10px 15px #00000080}[data-theme=light]{--bg-base:#fafcfc;--bg-raised:#f1f5f5;--bg-surface:#e8eded;--bg-card:#fff;--bg-card-hover:#f4f8f8;--border-subtle:#0000000a;--border-default:#00000014;--border-strong:#00000024;--border-glow:#0d948859;--text-primary:#0a0f0e;--text-secondary:#3d5250;--text-muted:#7a908e;--text-accent:var(--iris-700);--glow-primary:#0d94880f;--glow-strong:#0d94881f;--shadow-sm:0 1px 2px #0000000f;--shadow-md:0 4px 6px #00000014;--shadow-lg:0 10px 15px #0000001a}:root{--font-display:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-body:"Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", ui-monospace, monospace;--font-sans:var(--font-body);--text-caption-xs:11px;--text-caption:12px;--text-body-sm:13px;--text-body:14px;--text-body-lg:15px;--text-heading-sm:16px;--text-heading:20px;--text-display-sm:28px;--text-display:40px;--font-size-xs:var(--text-caption);--font-size-sm:var(--text-body-sm);--font-size-base:var(--text-body);--font-size-lg:var(--text-body-lg);--font-size-xl:var(--text-heading-sm);--font-size-2xl:var(--text-heading);--font-size-3xl:var(--text-display-sm);--leading-body:1.5;--leading-heading:1.2;--leading-display:1.1;--leading-mono:1.4;--space-0_5:2px;--space-1:4px;--space-1_5:6px;--space-2:8px;--space-2_5:10px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;--space-24:96px}:root,[data-density=compact]{--density-row:32px;--density-padding:var(--space-3);--density-body:var(--text-body-sm)}[data-density=comfortable]{--density-row:44px;--density-padding:var(--space-4);--density-body:var(--text-body)}:root{--sidebar-width-expanded:256px;--sidebar-width-collapsed:64px;--header-height:56px;--page-toolbar-height:40px;--radius-xs:4px;--radius-sm:6px;--radius:8px;--radius-lg:12px;--radius-xl:16px;--radius-pill:999px;--border-radius:var(--radius);--border-radius-sm:var(--radius-xs);--border-radius-lg:var(--radius-lg);--transition-instant:.1s ease;--transition-fast:.15s ease;--transition-base:.2s ease;--transition-slow:.3s ease;--ease-iris:cubic-bezier(.25, .4, .25, 1);--bg-primary:var(--bg-base);--bg-secondary:var(--bg-raised);--bg-tertiary:var(--bg-surface);--bg-hover:var(--bg-card-hover);--border-color:var(--border-default);--accent-primary:var(--iris-500);--accent-primary-hover:var(--iris-400);--accent-success:var(--eval-pass);--accent-error:var(--eval-fail);--accent-warning:var(--eval-warn);--accent-tool:var(--eval-tool);--accent-llm:var(--eval-llm)}html{transition:background-color var(--transition-base), color var(--transition-base)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{width:100%;height:100%}body{font-family:var(--font-body);font-size:var(--text-body);color:var(--text-primary);background-color:var(--bg-base);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;font-feature-settings:"cv11", "ss01"}h1,h2,h3,h4,.display{font-family:var(--font-display);letter-spacing:-.01em;font-weight:600}a{color:var(--text-accent);text-decoration:none}a:hover{color:var(--iris-300)}button{cursor:pointer;font-family:inherit}input,select{font-family:inherit;font-size:inherit}code,pre{font-family:var(--font-mono)}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,[role=button]:focus-visible{outline:2px solid var(--iris-500);outline-offset:2px;border-radius:var(--radius-xs)}::selection{background:var(--iris-600);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:var(--radius-xs)}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}html{scrollbar-color:var(--border-strong) transparent;scrollbar-width:thin}@keyframes pulse-ring{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(2.5)}}.pulse-dot{position:relative}.pulse-dot:after{content:"";background:var(--iris-500);border-radius:50%;animation:2s ease-out infinite pulse-ring;position:absolute;inset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (width<=767px){aside[aria-label=Main\ navigation]{width:160px}main{overflow-x:auto}}@media print{body{color:#000!important;background:#fff!important}aside[aria-label=Main\ navigation],header,[role=region][aria-label=Welcome],[role=region][aria-label=Bulk\ actions],[role=status],[role=dialog]{display:none!important}body,#root,main{height:auto!important;overflow:visible!important}main{padding:0!important}tr,pre,code{page-break-inside:avoid}h1,h2,h3{page-break-after:avoid}[aria-label*=violation],[aria-label*=spike],[aria-label*=collision],[aria-label*=Pass],[aria-label*=Fail]{border:1px solid #000!important}a{color:#000!important;text-decoration:underline!important}a[href^=http]:after{content:" (" attr(href) ")";color:#555;font-size:80%}}
|
|
1
|
+
@import "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500..700&family=Manrope:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap";:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--iris-50:#f0fdfa;--iris-100:#ccfbf1;--iris-200:#99f6e4;--iris-300:#5eead4;--iris-400:#2dd4bf;--iris-500:#14b8a6;--iris-600:#0d9488;--iris-700:#0f766e;--iris-800:#115e59;--iris-900:#134e4a;--iris-950:#042f2e;--eval-pass:#22c55e;--eval-warn:#eab308;--eval-fail:#ef4444;--eval-tool:#3b82f6;--eval-llm:#a855f7;--eval-skipped:#71717a}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root,[data-theme=dark]{--bg-base:#050508;--bg-raised:#08080e;--bg-surface:#0d0d15;--bg-card:#101018;--bg-card-hover:#16161f;--border-subtle:#ffffff0d;--border-default:#ffffff14;--border-strong:#ffffff24;--border-glow:#14b8a680;--text-primary:#f0f0f5;--text-secondary:#9494a8;--text-muted:#5e5e72;--text-accent:var(--iris-400);--glow-primary:#14b8a61f;--glow-strong:#14b8a640;--shadow-sm:0 1px 2px #0000004d;--shadow-md:0 4px 6px #0006;--shadow-lg:0 10px 15px #00000080}[data-theme=light]{--bg-base:#fafcfc;--bg-raised:#f1f5f5;--bg-surface:#e8eded;--bg-card:#fff;--bg-card-hover:#f4f8f8;--border-subtle:#0000000a;--border-default:#00000014;--border-strong:#00000024;--border-glow:#0d948859;--text-primary:#0a0f0e;--text-secondary:#3d5250;--text-muted:#7a908e;--text-accent:var(--iris-700);--glow-primary:#0d94880f;--glow-strong:#0d94881f;--shadow-sm:0 1px 2px #0000000f;--shadow-md:0 4px 6px #00000014;--shadow-lg:0 10px 15px #0000001a}:root{--font-display:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-body:"Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", ui-monospace, monospace;--font-sans:var(--font-body);--text-caption-xs:11px;--text-caption:12px;--text-body-sm:13px;--text-body:14px;--text-body-lg:15px;--text-heading-sm:16px;--text-heading:20px;--text-display-sm:28px;--text-display:40px;--font-size-xs:var(--text-caption);--font-size-sm:var(--text-body-sm);--font-size-base:var(--text-body);--font-size-lg:var(--text-body-lg);--font-size-xl:var(--text-heading-sm);--font-size-2xl:var(--text-heading);--font-size-3xl:var(--text-display-sm);--leading-body:1.5;--leading-heading:1.2;--leading-display:1.1;--leading-mono:1.4;--space-0_5:2px;--space-1:4px;--space-1_5:6px;--space-2:8px;--space-2_5:10px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;--space-24:96px}:root,[data-density=compact]{--density-row:32px;--density-padding:var(--space-3);--density-body:var(--text-body-sm)}[data-density=comfortable]{--density-row:44px;--density-padding:var(--space-4);--density-body:var(--text-body)}:root{--sidebar-width-expanded:256px;--sidebar-width-collapsed:64px;--header-height:56px;--page-toolbar-height:40px;--radius-xs:4px;--radius-sm:6px;--radius:8px;--radius-lg:12px;--radius-xl:16px;--radius-pill:999px;--border-radius:var(--radius);--border-radius-sm:var(--radius-xs);--border-radius-lg:var(--radius-lg);--transition-instant:.1s ease;--transition-fast:.15s ease;--transition-base:.2s ease;--transition-slow:.3s ease;--ease-iris:cubic-bezier(.25, .4, .25, 1);--bg-primary:var(--bg-base);--bg-secondary:var(--bg-raised);--bg-tertiary:var(--bg-surface);--bg-hover:var(--bg-card-hover);--border-color:var(--border-default);--accent-primary:var(--iris-500);--accent-primary-hover:var(--iris-400);--accent-success:var(--eval-pass);--accent-error:var(--eval-fail);--accent-warning:var(--eval-warn);--accent-tool:var(--eval-tool);--accent-llm:var(--eval-llm)}html{transition:background-color var(--transition-base), color var(--transition-base)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{width:100%;height:100%}body{font-family:var(--font-body);font-size:var(--text-body);color:var(--text-primary);background-color:var(--bg-base);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;font-feature-settings:"cv11", "ss01"}h1,h2,h3,h4,.display{font-family:var(--font-display);letter-spacing:-.01em;font-weight:600}a{color:var(--text-accent);text-decoration:none}a:hover{color:var(--iris-300)}button{cursor:pointer;font-family:inherit}input,select{font-family:inherit;font-size:inherit}code,pre{font-family:var(--font-mono)}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,[role=button]:focus-visible{outline:2px solid var(--iris-500);outline-offset:2px;border-radius:var(--radius-xs)}.iris-sr-reveal{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.iris-sr-reveal:focus-within{width:auto;height:auto;margin:var(--space-3) 0 0 0;padding:var(--space-3) var(--space-4);clip:auto;white-space:normal;background:var(--bg-card);color:var(--text-primary);border:1px solid var(--iris-500);border-radius:var(--radius-sm);position:static;overflow:visible}.iris-sr-reveal:focus-within>li{padding:var(--space-1) 0;list-style:none}::selection{background:var(--iris-600);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:var(--radius-xs)}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}html{scrollbar-color:var(--border-strong) transparent;scrollbar-width:thin}@keyframes pulse-ring{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(2.5)}}.pulse-dot{position:relative}.pulse-dot:after{content:"";background:var(--iris-500);border-radius:50%;animation:2s ease-out infinite pulse-ring;position:absolute;inset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (width<=767px){aside[aria-label=Main\ navigation]{width:160px}main{overflow-x:auto}}@media print{body{color:#000!important;background:#fff!important}aside[aria-label=Main\ navigation],header,[role=region][aria-label=Welcome],[role=region][aria-label=Bulk\ actions],[role=status],[role=dialog]{display:none!important}body,#root,main{height:auto!important;overflow:visible!important}main{padding:0!important}tr,pre,code{page-break-inside:avoid}h1,h2,h3{page-break-after:avoid}[aria-label*=violation],[aria-label*=spike],[aria-label*=collision],[aria-label*=Pass],[aria-label*=Fail]{border:1px solid #000!important}a{color:#000!important;text-decoration:underline!important}a[href^=http]:after{content:" (" attr(href) ")";color:#555;font-size:80%}}
|