@iris-eval/mcp-server 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +94 -44
  2. package/dist/audit-log-reader.d.ts +0 -2
  3. package/dist/audit-log-reader.js +3 -3
  4. package/dist/config/index.js +18 -1
  5. package/dist/custom-rule-store.js +22 -8
  6. package/dist/dashboard/assets/index-BZZt8bVh.js +10 -0
  7. package/dist/dashboard/assets/index-UffZ-aEJ.css +1 -0
  8. package/dist/dashboard/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
  9. package/dist/dashboard/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
  10. package/dist/dashboard/fonts/jetbrains-mono-greek.woff2 +0 -0
  11. package/dist/dashboard/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  12. package/dist/dashboard/fonts/jetbrains-mono-latin.woff2 +0 -0
  13. package/dist/dashboard/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
  14. package/dist/dashboard/fonts/manrope-cyrillic-ext.woff2 +0 -0
  15. package/dist/dashboard/fonts/manrope-cyrillic.woff2 +0 -0
  16. package/dist/dashboard/fonts/manrope-greek.woff2 +0 -0
  17. package/dist/dashboard/fonts/manrope-latin-ext.woff2 +0 -0
  18. package/dist/dashboard/fonts/manrope-latin.woff2 +0 -0
  19. package/dist/dashboard/fonts/manrope-vietnamese.woff2 +0 -0
  20. package/dist/dashboard/fonts/space-grotesk-latin-ext.woff2 +0 -0
  21. package/dist/dashboard/fonts/space-grotesk-latin.woff2 +0 -0
  22. package/dist/dashboard/fonts/space-grotesk-vietnamese.woff2 +0 -0
  23. package/dist/dashboard/index.html +2 -2
  24. package/dist/dashboard/routes/failures.d.ts +3 -0
  25. package/dist/dashboard/routes/failures.js +76 -0
  26. package/dist/dashboard/routes/index.d.ts +1 -0
  27. package/dist/dashboard/routes/index.js +1 -0
  28. package/dist/dashboard/routes/preferences.js +7 -2
  29. package/dist/dashboard/routes/rules.js +32 -14
  30. package/dist/dashboard/routes/traces.d.ts +12 -1
  31. package/dist/dashboard/routes/traces.js +90 -2
  32. package/dist/dashboard/seed-demo-data.d.ts +49 -0
  33. package/dist/dashboard/seed-demo-data.js +1080 -0
  34. package/dist/dashboard/server.js +81 -15
  35. package/dist/dashboard/validation.d.ts +74 -0
  36. package/dist/dashboard/validation.js +31 -2
  37. package/dist/eval/citation-verify/resolve.js +29 -0
  38. package/dist/eval/citation-verify/verifier.d.ts +1 -0
  39. package/dist/eval/citation-verify/verifier.js +12 -4
  40. package/dist/eval/engine.d.ts +15 -1
  41. package/dist/eval/engine.js +74 -5
  42. package/dist/eval/failure-rank.d.ts +14 -0
  43. package/dist/eval/failure-rank.js +44 -0
  44. package/dist/eval/rules/custom.d.ts +29 -1
  45. package/dist/eval/rules/custom.js +155 -19
  46. package/dist/eval/rules/regex-budget.js +0 -0
  47. package/dist/eval/rules/regex-sandbox.d.ts +26 -0
  48. package/dist/eval/rules/regex-sandbox.js +131 -0
  49. package/dist/eval/rules/relevance.d.ts +0 -2
  50. package/dist/eval/rules/relevance.js +6 -68
  51. package/dist/eval/rules/safety.d.ts +10 -0
  52. package/dist/eval/rules/safety.js +1337 -26
  53. package/dist/index.js +196 -18
  54. package/dist/self-test.d.ts +18 -0
  55. package/dist/self-test.js +329 -0
  56. package/dist/storage/sqlite-adapter.d.ts +2 -0
  57. package/dist/storage/sqlite-adapter.js +65 -9
  58. package/dist/tools/delete-rule.d.ts +2 -1
  59. package/dist/tools/delete-rule.js +13 -4
  60. package/dist/tools/delete-trace.js +2 -1
  61. package/dist/tools/deploy-rule.d.ts +2 -1
  62. package/dist/tools/deploy-rule.js +29 -7
  63. package/dist/tools/evaluate-output.js +36 -9
  64. package/dist/tools/evaluate-with-llm-judge.js +2 -1
  65. package/dist/tools/get-traces.js +6 -2
  66. package/dist/tools/index.js +2 -2
  67. package/dist/tools/list-rules.js +2 -1
  68. package/dist/tools/log-trace.d.ts +51 -0
  69. package/dist/tools/log-trace.js +14 -2
  70. package/dist/tools/strict-input.d.ts +2 -0
  71. package/dist/tools/strict-input.js +35 -0
  72. package/dist/tools/verify-citations.js +7 -5
  73. package/dist/transport/http.js +24 -2
  74. package/dist/types/decision-moment.d.ts +12 -0
  75. package/dist/types/eval.d.ts +32 -0
  76. package/dist/types/query.d.ts +1 -1
  77. package/dist/utils/write-atomic.d.ts +2 -0
  78. package/dist/utils/write-atomic.js +34 -2
  79. package/package.json +3 -2
  80. package/server.json +3 -3
  81. package/dist/dashboard/assets/index-B4Aw6ozt.css +0 -1
  82. package/dist/dashboard/assets/index-ChcHJDDJ.js +0 -10
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # Iris — The Agent Eval Standard for MCP
1
+ # Iris — stop shipping agents on vibes
2
2
 
3
3
  [![Glama Score](https://glama.ai/mcp/servers/iris-eval/mcp-server/badges/score.svg)](https://glama.ai/mcp/servers/iris-eval/mcp-server)
4
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBpcmlzLWV2YWwvbWNwLXNlcnZlciJdLCJlbnYiOnsiSVJJU19MT0dfTEVWRUwiOiJpbmZvIn19)
4
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=iris-eval&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBpcmlzLWV2YWwvbWNwLXNlcnZlciJdLCJlbnYiOnsiSVJJU19MT0dfTEVWRUwiOiJpbmZvIn19)
5
5
  [![npm version](https://img.shields.io/npm/v/@iris-eval/mcp-server)](https://npmjs.com/package/@iris-eval/mcp-server)
6
6
  [![npm downloads](https://img.shields.io/npm/dt/@iris-eval/mcp-server)](https://npmjs.com/package/@iris-eval/mcp-server)
7
7
  [![GitHub stars](https://img.shields.io/github/stars/iris-eval/mcp-server?style=social)](https://github.com/iris-eval/mcp-server)
@@ -13,70 +13,77 @@
13
13
  [![PulseMCP](https://img.shields.io/badge/PulseMCP-Listed-blue?style=flat-square)](https://www.pulsemcp.com/servers/iris-eval)
14
14
  [![mcp.so](https://img.shields.io/badge/mcp.so-Listed-blue?style=flat-square)](https://mcp.so/server/iris/iris-eval)
15
15
 
16
- **Know whether your AI agents are actually good enough to ship.** Iris is an open-source MCP server that scores output quality, catches safety failures, and enforces cost budgets across all your agents. Any MCP-compatible agent discovers and uses it automatically no SDK, no code changes.
16
+ **Iris scores every agent run for quality, safety, and cost — on your machine, with no SDK and no account.** Most agent projects check quality by running a few remembered prompts and eyeballing the output. Iris replaces that with numbers you can audit: your agent's runs land in a SQLite database on your disk, 13 built-in rules score them deterministically PII, prompt injection, hallucination markers, cost thresholds — free, with no LLM calls, and an optional LLM judge with a hard per-eval cost cap handles the semantic questions. Every rule is inspectable and editable, because a judge you can't audit is just vibes with a number on it. MIT licensed, no telemetry; your traces never leave your machine.
17
+
18
+ **Requires Node.js 20 or later.** Check with `node --version`.
17
19
 
18
20
  ![Iris Dashboard](https://raw.githubusercontent.com/iris-eval/mcp-server/main/docs/assets/dashboard-overview.png)
19
21
 
20
- ## The Problem
22
+ ## A failure on screen in 60 seconds
21
23
 
22
- Your agents are running in production. Infrastructure monitoring sees `200 OK` and moves on. It has no idea the agent just:
24
+ No agent wiring, no config one command:
23
25
 
24
- - Leaked a social security number in its response
25
- - Hallucinated an answer with zero factual grounding
26
- - Burned $0.47 on a single query — 4.7x your budget threshold
27
- - Made 6 tool calls when 2 would have sufficed
26
+ ```bash
27
+ npx @iris-eval/mcp-server --demo
28
+ ```
28
29
 
29
- Iris evaluates all of it.
30
+ This seeds a demo database — a handful of small agents with a week of runs — and serves the dashboard against it at **http://localhost:6920** (your browser opens automatically on first run). The dashboard lands on **Failures**: what failed, worst and newest first. Worth clicking into — a PII leak caught by the safety rules, a flagged prompt-injection attempt, and a failed LLM-judge score with its rationale.
30
31
 
31
- ## What You Get
32
+ Demo data lives in its own database (`demo.db` in your Iris home directory — `~/.iris` on macOS/Linux, `%USERPROFILE%\.iris` on Windows) and never mixes with your real traces. Remove all of it with one command:
32
33
 
33
- | | |
34
- |---|---|
35
- | **Trace Logging** | Hierarchical span trees with per-tool-call latency, token usage, and cost in USD. Stored in SQLite, queryable instantly. |
36
- | **Output Evaluation** | 13 built-in rules across 4 categories: completeness, relevance, safety, cost. PII detection (10 patterns: SSN, credit card, phone, email, IBAN, DOB, MRN, IP, API key, passport), prompt injection (13 patterns), stub-output detection, hallucination markers (17 hedging phrases + fabricated-citation heuristic). Add custom rules with Zod schemas. |
37
- | **Cost Visibility** | Aggregate cost across all agents over any time window. Set budget thresholds. Get flagged when agents overspend. |
38
- | **Web Dashboard** | Real-time dark-mode UI with trace visualization, eval results, and cost breakdowns. |
39
-
40
- **Requires Node.js 20 or later.** Check with `node --version`.
34
+ ```bash
35
+ npx @iris-eval/mcp-server --demo-clear
36
+ ```
41
37
 
42
- ## Quickstart
38
+ ## Hook up your own agent
43
39
 
44
- Add Iris to your MCP config. Works with Claude Desktop, Claude Code, Cursor, Windsurf, Continue, VS Code, Cline, Zed, Codex CLI, Gemini CLI — and any other MCP-compatible agent.
40
+ Add Iris to your MCP config. Works with Claude Desktop, Claude Code, Cursor, Windsurf, Continue, VS Code, Cline, Zed, Codex CLI, Gemini CLI — and any other MCP-compatible agent. One block, dashboard included:
45
41
 
46
42
  ```json
47
43
  {
48
44
  "mcpServers": {
49
45
  "iris-eval": {
50
46
  "command": "npx",
51
- "args": ["@iris-eval/mcp-server"]
47
+ "args": ["@iris-eval/mcp-server", "--dashboard"]
52
48
  }
53
49
  }
54
50
  }
55
51
  ```
56
52
 
57
- That's it. Your agent discovers Iris and starts logging traces automatically.
53
+ Your agent discovers Iris's nine tools on connect, and the dashboard serves at **http://localhost:6920**. Now paste this to your agent:
58
54
 
59
- ### Turn on the dashboard
55
+ > Log that last task to Iris and evaluate the output.
60
56
 
61
- Iris ships with a real-time web dashboard showing traces, eval results, cost breakdowns, and rule pass-rates. It's off by default so the MCP server stays lightweight flip it on with a flag.
57
+ The trace lands on the dashboard with its scores. Prefer the MCP server headless? Drop `--dashboard` from the args you can open the same dashboard any time with `npx @iris-eval/mcp-server --dashboard`.
62
58
 
63
- ```json
64
- {
65
- "mcpServers": {
66
- "iris-eval": {
67
- "command": "npx",
68
- "args": ["@iris-eval/mcp-server", "--dashboard"]
69
- }
70
- }
71
- }
59
+ **One thing worth knowing up front:** MCP tools are called when the model decides to call them. Iris doesn't intercept your agent, so traces are logged when your agent asks it to log them — either because you told it to, or because your code calls the tools directly. Ask your agent to "log this to Iris and evaluate it" and it will. If you want capture that doesn't depend on the model choosing, `POST /api/v1/traces` does exactly that — your code sends the trace over plain HTTP, no model in the loop (see [docs/http-ingest.md](docs/http-ingest.md)). The CLI and SDKs on the [roadmap](docs/roadmap.md) will be thin clients over the same endpoint.
60
+
61
+ ### Capture over HTTP (no model in the loop)
62
+
63
+ With the dashboard running, anything that can send an HTTP request can log a trace — and optionally run the deterministic evals in the same request:
64
+
65
+ ```bash
66
+ curl -s -X POST "http://127.0.0.1:6920/api/v1/traces" \
67
+ -H "Content-Type: application/json" \
68
+ -d '{
69
+ "agent_name": "support-bot",
70
+ "input": "What is the refund policy?",
71
+ "output": "Refunds are available within 30 days of purchase.",
72
+ "evaluate": true,
73
+ "eval_type": "safety"
74
+ }'
72
75
  ```
73
76
 
74
- Then open **http://localhost:6920** after your agent runs a trace. The same dashboard is available via CLI:
77
+ Returns `201` with the stored `trace_id` and the evaluation result. The endpoint accepts the same body as the `log_trace` tool and sits behind the same loopback-only middleware stack as the rest of the dashboard. Full contract, field reference, and error semantics: [docs/http-ingest.md](docs/http-ingest.md).
78
+
79
+ ### Check the install
75
80
 
76
81
  ```bash
77
- npx @iris-eval/mcp-server --dashboard
82
+ npx @iris-eval/mcp-server --self-test
78
83
  ```
79
84
 
85
+ An offline install diagnostic: storage round-trip, deterministic evals, dashboard + DNS-rebinding guard — all inside an isolated temp home, so your real database is never opened. Exit code 0 = healthy, 1 = a check failed.
86
+
80
87
  <details>
81
88
  <summary><strong>Setup by tool</strong></summary>
82
89
 
@@ -171,6 +178,19 @@ docker run -p 3000:3000 -v iris-data:/data ghcr.io/iris-eval/mcp-server
171
178
 
172
179
  > **Tip:** Global install (`npm install -g`) stores traces persistently at `~/.iris/iris.db`. With `npx`, traces persist in the same location, but startup is slower due to package resolution.
173
180
 
181
+ ## What You Get
182
+
183
+ | | |
184
+ |---|---|
185
+ | **Trace Logging** | Hierarchical span trees with per-tool-call latency, token usage, and cost in USD. Stored in SQLite, queryable instantly. |
186
+ | **Output Evaluation** | 13 built-in rules across 4 categories: completeness, relevance, safety, cost. PII detection (19 patterns: SSN, credit card, phone, email, IBAN, DOB, MRN, IP, API key, passport, plus AWS/Slack/SendGrid/GitHub/Google/npm/DigitalOcean tokens, PEM private-key blocks and seed phrases), prompt injection (37 patterns, phrase + structural), stub-output detection, hallucination detection (25 context-grounded fabrication/contradiction signals — pass `input` to ground them against the agent's source material). Add custom rules with Zod schemas. |
187
+ | **LLM-as-Judge** | Optional semantic scoring via Anthropic or OpenAI — bring your own API key. Five templates. Hard per-eval cost cap (`IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL`, default $0.25), per-eval pricing disclosed in the result. |
188
+ | **Cost Visibility** | Aggregate cost across all agents over any time window. Set budget thresholds. Get flagged when agents overspend. |
189
+ | **Web Dashboard** | Real-time dark-mode UI that lands on the failures, worst and newest first — trace visualization, eval results, cost breakdowns, and a command palette (⌘K) that searches your own rules, traces, and evals. |
190
+ | **Local-first** | Everything lives in SQLite on your disk. No account, no sign-up, no telemetry. Outbound HTTP happens only where you opt in: your own LLM-judge key, citation fetching, or an OTel exporter you configure. |
191
+
192
+ Where this is going next: [the roadmap](docs/roadmap.md).
193
+
174
194
  ## MCP Tools
175
195
 
176
196
  Iris registers nine tools that any MCP-compatible agent can invoke — full rule + trace lifecycle + LLM-as-judge + semantic citation verification:
@@ -187,13 +207,26 @@ Iris registers nine tools that any MCP-compatible agent can invoke — full rule
187
207
 
188
208
  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).
189
209
 
210
+ ### How `passed` is decided
211
+
212
+ `evaluate_output` returns both a `score` and a `passed` flag — they answer different questions:
213
+
214
+ - **`score`** (0..1) is the weighted average across the rules that ran — a quality gradient.
215
+ - **`passed`** is the ship/no-ship verdict: `true` only when the score clears the pass threshold (default **0.7**) **and no critical rule failed**.
216
+
217
+ Genuine safety violations hard-fail. `no_pii`, `no_injection_patterns`, and `no_blocklist_words` are **critical rules**: if one fails, the eval reports `passed: false` no matter how well the other rules scored, and the response names the culprits in `critical_failures`. A leaked SSN can't be averaged away. Custom rules deployed with `severity: "high"` or `"critical"` hard-fail the same way; `low`/`medium` severities only affect the score. One boundary to know: a critical rule that **skipped** (missing context, or any other cause of a skip) has not judged the output and does not veto — `rule_results` shows every skip and its reason, so a gate that must fail closed on non-verdicts can.
218
+
219
+ One gotcha for CI gates: if you omit `eval_type`, the default `completeness` bundle runs — **safety rules don't**. The response echoes `eval_type` (plus a `note` when it was defaulted) so your gate can verify which bundle actually ran. Key on `passed` for the verdict and `eval_type: "safety"` for coverage.
220
+
190
221
  Full tool schemas and configuration: [iris-eval.com](https://iris-eval.com)
191
222
 
192
- ## Cloud Tier (Coming Soon)
223
+ ## Hosted features
224
+
225
+ Iris runs entirely on your machine today, and everything it does is free and MIT licensed with no limits and no account.
193
226
 
194
- Self-hosted Iris runs on your machine with SQLite. As your team's eval needs grow, the cloud tier adds PostgreSQL, team dashboards, alerting on quality regressions, and managed infrastructure.
227
+ Hosted storage, shared team history and alerting are **under consideration, not under construction**. There is no pricing, and nothing to buy. If shared history would be useful to you, [the waitlist](https://iris-eval.com#waitlist) is how we find out whether it's worth building it commits you to nothing.
195
228
 
196
- [Join the waitlist](https://iris-eval.com#waitlist) to get early access.
229
+ Two commitments hold regardless: **nothing that is free today will move behind a paywall**, and **no compliance certification will be claimed before it is held**.
197
230
 
198
231
  ## Examples
199
232
 
@@ -208,6 +241,7 @@ Self-hosted Iris runs on your machine with SQLite. As your team's eval needs gro
208
241
  - [GitHub Issues](https://github.com/iris-eval/mcp-server/issues) — Bug reports and feature requests
209
242
  - [GitHub Discussions](https://github.com/iris-eval/mcp-server/discussions) — Questions and ideas
210
243
  - [Contributing Guide](CONTRIBUTING.md) — How to contribute
244
+ - [HTTP Ingest](docs/http-ingest.md) — Deterministic trace capture via `POST /api/v1/traces`
211
245
  - [Roadmap](docs/roadmap.md) — What's coming next
212
246
 
213
247
  <details>
@@ -224,6 +258,10 @@ Self-hosted Iris runs on your machine with SQLite. As your team's eval needs gro
224
258
  | `--api-key` | — | API key for HTTP authentication |
225
259
  | `--dashboard` | `false` | Enable web dashboard |
226
260
  | `--dashboard-port` | `6920` | Dashboard port |
261
+ | `--dashboard-host` | `127.0.0.1` | Dashboard bind address. Loopback by default — the dashboard is unauthenticated unless `--api-key` is set, so binding beyond loopback exposes your full trace history |
262
+ | `--demo` | `false` | Seed a demo database (separate from your real traces) and serve the dashboard against it |
263
+ | `--demo-clear` | `false` | Delete the demo database and exit |
264
+ | `--self-test` | `false` | Run the offline install diagnostic in an isolated temp home, then exit (0 = healthy, 1 = a check failed) |
227
265
 
228
266
  ### Environment Variables
229
267
 
@@ -232,10 +270,12 @@ Self-hosted Iris runs on your machine with SQLite. As your team's eval needs gro
232
270
  | `IRIS_TRANSPORT` | Transport type (`stdio` or `http`) |
233
271
  | `IRIS_PORT` | HTTP transport port |
234
272
  | `IRIS_HOST` | HTTP transport host (default `127.0.0.1`) |
235
- | `IRIS_DB_PATH` | SQLite database path |
273
+ | `IRIS_HOME` | Directory for all per-user files: `config.json`, `iris.db`, `custom-rules.json`, `audit.log`, `preferences.json` (default `~/.iris`) |
274
+ | `IRIS_DB_PATH` | SQLite database path (overrides `IRIS_HOME` for the DB only) |
236
275
  | `IRIS_LOG_LEVEL` | Log level: `debug`, `info`, `warn`, `error` |
237
- | `IRIS_DASHBOARD` | Enable web dashboard (`true`/`false`) |
276
+ | `IRIS_DASHBOARD` | Enable web dashboard (`true`/`false`; `false` also overrides `dashboard.enabled` in config.json) |
238
277
  | `IRIS_DASHBOARD_PORT` | Dashboard port (default `6920`) |
278
+ | `IRIS_DASHBOARD_HOST` | Dashboard bind address (default `127.0.0.1`) |
239
279
  | `IRIS_API_KEY` | API key for HTTP authentication |
240
280
  | `IRIS_ALLOWED_ORIGINS` | Comma-separated allowed CORS origins |
241
281
 
@@ -263,6 +303,14 @@ iris-mcp --transport http --port 3000 --api-key "$(openssl rand -hex 32)" --dash
263
303
  <details>
264
304
  <summary><strong>Troubleshooting</strong></summary>
265
305
 
306
+ ### First move: run the self-test
307
+
308
+ ```bash
309
+ npx @iris-eval/mcp-server --self-test
310
+ ```
311
+
312
+ It checks storage, the deterministic evals, and the dashboard in an isolated temp home and prints a per-step verdict — the failure output names the broken step. Exit code 0 means the install is healthy.
313
+
266
314
  ### Iris won't start / `ERR_MODULE_NOT_FOUND`
267
315
 
268
316
  You may have a cached older version. Clear the npx cache and retry:
@@ -283,13 +331,15 @@ MCP tools only load at session start. After adding iris-eval, restart the sessio
283
331
 
284
332
  ### Version check
285
333
 
286
- Verify which version is running:
334
+ Iris logs its version on the first startup line:
287
335
 
288
336
  ```bash
289
- npx @iris-eval/mcp-server --help
290
- # Shows "Iris MCP-Native Agent Eval Server vX.Y.Z"
337
+ npx @iris-eval/mcp-server --dashboard
338
+ # First log line: "Starting Iris MCP server vX.Y.Z"
291
339
  ```
292
340
 
341
+ For a global install, `npm ls -g @iris-eval/mcp-server` shows the installed version.
342
+
293
343
  ### Updating
294
344
 
295
345
  ```bash
@@ -12,8 +12,6 @@ export interface AuditQueryResult {
12
12
  total: number;
13
13
  limit: number;
14
14
  offset: number;
15
- /** Absolute path to the audit log file (for diagnostics). */
16
- path: string;
17
15
  }
18
16
  export declare function readAuditLog(opts?: {
19
17
  filePath?: string;
@@ -38,14 +38,14 @@ export function readAuditLog(opts) {
38
38
  const limit = Math.min(Math.max(opts?.limit ?? 100, 1), 1000);
39
39
  const offset = Math.max(opts?.offset ?? 0, 0);
40
40
  if (!existsSync(filePath)) {
41
- return { entries: [], total: 0, limit, offset, path: filePath };
41
+ return { entries: [], total: 0, limit, offset };
42
42
  }
43
43
  let raw;
44
44
  try {
45
45
  raw = readFileSync(filePath, 'utf-8');
46
46
  }
47
47
  catch {
48
- return { entries: [], total: 0, limit, offset, path: filePath };
48
+ return { entries: [], total: 0, limit, offset };
49
49
  }
50
50
  // Parse line-by-line, drop malformed rows silently. The audit log is
51
51
  // append-only so a partially-flushed last line is the only realistic
@@ -83,5 +83,5 @@ export function readAuditLog(opts) {
83
83
  filtered.sort((a, b) => b.ts.localeCompare(a.ts));
84
84
  const total = filtered.length;
85
85
  const entries = filtered.slice(offset, offset + limit);
86
- return { entries, total, limit, offset, path: filePath };
86
+ return { entries, total, limit, offset };
87
87
  }
@@ -45,6 +45,23 @@ function parsePortEnv(value, name) {
45
45
  }
46
46
  return n;
47
47
  }
48
+ /*
49
+ * IRIS_DASHBOARD used to be `value === 'true'`, which silently read every
50
+ * other spelling — 1, yes, on, TRUE — as an explicit DISABLE that then
51
+ * overrode config.json's dashboard.enabled in the layer merge. The user who
52
+ * exported IRIS_DASHBOARD=1 got no dashboard plus a pointer log telling
53
+ * them to set the very variable they believed they had set. Unrecognized
54
+ * values now throw, same contract as parsePortEnv: loud beats silently
55
+ * wrong for a startup switch.
56
+ */
57
+ function parseBooleanEnv(value, name) {
58
+ const normalized = value.trim().toLowerCase();
59
+ if (['true', '1', 'yes', 'on'].includes(normalized))
60
+ return true;
61
+ if (['false', '0', 'no', 'off'].includes(normalized))
62
+ return false;
63
+ throw new Error(`${name}=${JSON.stringify(value)} is not a valid boolean (use true/1/yes/on or false/0/no/off)`);
64
+ }
48
65
  function loadEnvVars() {
49
66
  const config = {};
50
67
  if (process.env.IRIS_TRANSPORT) {
@@ -63,7 +80,7 @@ function loadEnvVars() {
63
80
  config.logging = { level: process.env.IRIS_LOG_LEVEL };
64
81
  }
65
82
  if (process.env.IRIS_DASHBOARD) {
66
- config.dashboard = { enabled: process.env.IRIS_DASHBOARD === 'true' };
83
+ config.dashboard = { enabled: parseBooleanEnv(process.env.IRIS_DASHBOARD, 'IRIS_DASHBOARD') };
67
84
  }
68
85
  if (process.env.IRIS_DASHBOARD_PORT) {
69
86
  config.dashboard = {
@@ -24,13 +24,14 @@
24
24
  * leave a half-file.
25
25
  */
26
26
  import { mkdirSync, readFileSync, existsSync, appendFileSync } from 'node:fs';
27
- import { writeAtomic } from './utils/write-atomic.js';
27
+ import { writeAtomic, ensureOwnerOnly, OWNER_ONLY_FILE_MODE } from './utils/write-atomic.js';
28
28
  import { irisHome } from './utils/iris-home.js';
29
29
  import { join, dirname } from 'node:path';
30
30
  import { randomBytes } from 'node:crypto';
31
31
  import { z } from 'zod';
32
32
  import isSafeRegex from 'safe-regex2';
33
33
  import { regexBacktrackingBudgetExceeded } from './eval/rules/regex-budget.js';
34
+ import { normalizeRegexSource } from './eval/rules/custom.js';
34
35
  import { CUSTOM_RULE_CONFIG_KEYS, readNumericConfig, describeKeys } from './eval/rules/config-keys.js';
35
36
  import { LOCAL_TENANT } from './types/tenant.js';
36
37
  const SEVERITY_VALUES = ['low', 'medium', 'high', 'critical'];
@@ -99,15 +100,18 @@ const DefinitionSchema = z
99
100
  });
100
101
  break;
101
102
  }
102
- // Strip a leading inline flag group the way the evaluator does, so a
103
- // pattern that WILL run is not rejected here for syntax it tolerates.
104
- const stripped = pattern.replace(/^\(\?[imsugy]+\)/, '');
103
+ // Normalize EXACTLY the way the evaluator does — same helper — so
104
+ // this layer validates and probes the identical pattern+flags pair
105
+ // that will actually run. (It used to strip the inline flag group
106
+ // but not merge its flags: a `(?i)` pattern was probed under
107
+ // different flags than evaluation used.)
108
+ const { pattern: stripped, flags: normalizedFlags } = normalizeRegexSource(pattern, typeof config.flags === 'string' ? config.flags : '');
105
109
  // Syntax BEFORE safety: safe-regex2 returns false for anything it
106
110
  // cannot parse, so checking it first reports a plainly broken pattern
107
111
  // like `(` as "catastrophic backtracking" — an error that sends the
108
112
  // author looking for a performance problem they do not have.
109
113
  try {
110
- new RegExp(stripped, typeof config.flags === 'string' ? config.flags : '');
114
+ new RegExp(stripped, normalizedFlags);
111
115
  }
112
116
  catch (e) {
113
117
  ctx.addIssue({
@@ -130,7 +134,7 @@ const DefinitionSchema = z
130
134
  // safe and takes 156ms on 40 characters. Measure what the static
131
135
  // check cannot see.
132
136
  {
133
- const budgetIssue = regexBacktrackingBudgetExceeded(stripped, typeof config.flags === 'string' ? config.flags : '');
137
+ const budgetIssue = regexBacktrackingBudgetExceeded(stripped, normalizedFlags);
134
138
  if (budgetIssue) {
135
139
  ctx.addIssue({
136
140
  code: z.ZodIssueCode.custom,
@@ -207,7 +211,13 @@ function generateRuleId() {
207
211
  function appendAudit(auditPath, entry) {
208
212
  try {
209
213
  mkdirSync(dirname(auditPath), { recursive: true });
210
- appendFileSync(auditPath, `${JSON.stringify(entry)}\n`, 'utf-8');
214
+ // mode applies only when appendFileSync creates the file; an existing
215
+ // audit.log keeps its mode, which is why ensureOwnerOnly() also runs at
216
+ // store construction to repair files created before this change.
217
+ appendFileSync(auditPath, `${JSON.stringify(entry)}\n`, {
218
+ encoding: 'utf-8',
219
+ mode: OWNER_ONLY_FILE_MODE,
220
+ });
211
221
  }
212
222
  catch {
213
223
  // Audit best-effort. If filesystem is read-only or full, the deploy
@@ -262,7 +272,11 @@ export function createCustomRuleStore(opts) {
262
272
  function state(tenantId) {
263
273
  let loaded = tenantState.get(tenantId);
264
274
  if (loaded === undefined) {
265
- loaded = loadRulesFromDisk(pathFor(tenantId));
275
+ const path = pathFor(tenantId);
276
+ loaded = loadRulesFromDisk(path);
277
+ // Repair permissions on files created before the owner-only change
278
+ // (and on the audit log, which appendFileSync only modes at creation).
279
+ ensureOwnerOnly(path, auditPath);
266
280
  tenantState.set(tenantId, loaded);
267
281
  }
268
282
  return loaded;