@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.
- package/README.md +94 -44
- package/dist/audit-log-reader.d.ts +0 -2
- package/dist/audit-log-reader.js +3 -3
- package/dist/config/index.js +18 -1
- package/dist/custom-rule-store.js +22 -8
- package/dist/dashboard/assets/index-BZZt8bVh.js +10 -0
- package/dist/dashboard/assets/index-UffZ-aEJ.css +1 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-greek.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/dashboard/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-cyrillic.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-greek.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-latin.woff2 +0 -0
- package/dist/dashboard/fonts/manrope-vietnamese.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-latin.woff2 +0 -0
- package/dist/dashboard/fonts/space-grotesk-vietnamese.woff2 +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/routes/failures.d.ts +3 -0
- package/dist/dashboard/routes/failures.js +76 -0
- package/dist/dashboard/routes/index.d.ts +1 -0
- package/dist/dashboard/routes/index.js +1 -0
- package/dist/dashboard/routes/preferences.js +7 -2
- package/dist/dashboard/routes/rules.js +32 -14
- package/dist/dashboard/routes/traces.d.ts +12 -1
- package/dist/dashboard/routes/traces.js +90 -2
- package/dist/dashboard/seed-demo-data.d.ts +49 -0
- package/dist/dashboard/seed-demo-data.js +1080 -0
- package/dist/dashboard/server.js +81 -15
- package/dist/dashboard/validation.d.ts +74 -0
- package/dist/dashboard/validation.js +31 -2
- package/dist/eval/citation-verify/resolve.js +29 -0
- package/dist/eval/citation-verify/verifier.d.ts +1 -0
- package/dist/eval/citation-verify/verifier.js +12 -4
- package/dist/eval/engine.d.ts +15 -1
- package/dist/eval/engine.js +74 -5
- package/dist/eval/failure-rank.d.ts +14 -0
- package/dist/eval/failure-rank.js +44 -0
- package/dist/eval/rules/custom.d.ts +29 -1
- package/dist/eval/rules/custom.js +155 -19
- package/dist/eval/rules/regex-budget.js +0 -0
- package/dist/eval/rules/regex-sandbox.d.ts +26 -0
- package/dist/eval/rules/regex-sandbox.js +131 -0
- package/dist/eval/rules/relevance.d.ts +0 -2
- package/dist/eval/rules/relevance.js +6 -68
- package/dist/eval/rules/safety.d.ts +10 -0
- package/dist/eval/rules/safety.js +1337 -26
- package/dist/index.js +196 -18
- package/dist/self-test.d.ts +18 -0
- package/dist/self-test.js +329 -0
- package/dist/storage/sqlite-adapter.d.ts +2 -0
- package/dist/storage/sqlite-adapter.js +65 -9
- package/dist/tools/delete-rule.d.ts +2 -1
- package/dist/tools/delete-rule.js +13 -4
- package/dist/tools/delete-trace.js +2 -1
- package/dist/tools/deploy-rule.d.ts +2 -1
- package/dist/tools/deploy-rule.js +29 -7
- package/dist/tools/evaluate-output.js +36 -9
- package/dist/tools/evaluate-with-llm-judge.js +2 -1
- package/dist/tools/get-traces.js +6 -2
- package/dist/tools/index.js +2 -2
- package/dist/tools/list-rules.js +2 -1
- package/dist/tools/log-trace.d.ts +51 -0
- package/dist/tools/log-trace.js +14 -2
- package/dist/tools/strict-input.d.ts +2 -0
- package/dist/tools/strict-input.js +35 -0
- package/dist/tools/verify-citations.js +7 -5
- package/dist/transport/http.js +24 -2
- package/dist/types/decision-moment.d.ts +12 -0
- package/dist/types/eval.d.ts +32 -0
- package/dist/types/query.d.ts +1 -1
- package/dist/utils/write-atomic.d.ts +2 -0
- package/dist/utils/write-atomic.js +34 -2
- package/package.json +3 -2
- package/server.json +3 -3
- package/dist/dashboard/assets/index-B4Aw6ozt.css +0 -1
- package/dist/dashboard/assets/index-ChcHJDDJ.js +0 -10
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Iris —
|
|
1
|
+
# Iris — stop shipping agents on vibes
|
|
2
2
|
|
|
3
3
|
[](https://glama.ai/mcp/servers/iris-eval/mcp-server)
|
|
4
|
-
[](cursor://anysphere.cursor-deeplink/mcp/install?name=
|
|
4
|
+
[](cursor://anysphere.cursor-deeplink/mcp/install?name=iris-eval&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBpcmlzLWV2YWwvbWNwLXNlcnZlciJdLCJlbnYiOnsiSVJJU19MT0dfTEVWRUwiOiJpbmZvIn19)
|
|
5
5
|
[](https://npmjs.com/package/@iris-eval/mcp-server)
|
|
6
6
|
[](https://npmjs.com/package/@iris-eval/mcp-server)
|
|
7
7
|
[](https://github.com/iris-eval/mcp-server)
|
|
@@ -13,70 +13,77 @@
|
|
|
13
13
|
[](https://www.pulsemcp.com/servers/iris-eval)
|
|
14
14
|
[](https://mcp.so/server/iris/iris-eval)
|
|
15
15
|
|
|
16
|
-
**
|
|
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
|

|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## A failure on screen in 60 seconds
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
No agent wiring, no config — one command:
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
- Made 6 tool calls when 2 would have sufficed
|
|
26
|
+
```bash
|
|
27
|
+
npx @iris-eval/mcp-server --demo
|
|
28
|
+
```
|
|
28
29
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
> Log that last task to Iris and evaluate the output.
|
|
60
56
|
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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 --
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
334
|
+
Iris logs its version on the first startup line:
|
|
287
335
|
|
|
288
336
|
```bash
|
|
289
|
-
npx @iris-eval/mcp-server --
|
|
290
|
-
#
|
|
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
|
package/dist/audit-log-reader.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
86
|
+
return { entries, total, limit, offset };
|
|
87
87
|
}
|
package/dist/config/index.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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;
|