Repository navigation
[0.21.0] - 2026-10-25
A security release: what an agent or a client sends Iris is bounded before Iris reads it, and a tool argument can narrow what the operator allows but never widen it. 0.21.0:
- A tool call, a dashboard API body and an OTLP batch are each held to
security.maxInputElements(100,000 array elements and object keys by default) before any schema reads them, and a validation error lists at most 20 issues and the total. verify_citationsandevaluate_with_llm_judgetake the operator's fetch setting, domain list and cost caps as ceilings; every judge call on a key draws on one daily budget; and stored text comes back inside a fence that marks it as text someone else wrote.- With an allowlist set, the citation fetcher looks up only the hosts on it.
- The lowest versions Iris accepts are now
@modelcontextprotocol/sdk1.32.0,body-parser2.4.0 andzod4.6.7, and the Docker image runs Node.js 24.22.1. - Also:
npm run preflight, which runs what CI will run before you push, and proof measured on agent runs Iris did not write.
Check before upgrading. Three entries under Security are marked BREAKING. Read them if an agent passes allow_fetch, domain_allowlist, max_cost_usd or max_cost_usd_total to Iris's tools, if the judge tools spend more than 1 USD a day on your key (IRIS_LLM_JUDGE_DAILY_BUDGET_USD), or if a script reads stored text from get_traces, list_rules or a trace resource.
Added
-
npm run preflight: what CI will say, before you push. It runs, in CI's order, every check a pull request's workflows run that can run on your machine: the lint, the type checks, the proof checks, the builds, every test with the coverage floors, and the truthbase regenerated and compared with the committed one, then records the tree it verified.git config core.hooksPath scripts/git-hooksturns on a pre-push hook that refuses a branch the preflight has not passed at that tree. A test holds it to the workflows: every job a pull request runs is a step or is listed as CI-only with the reason (Playwright, the real MCP clients, Docker, the Python client and the other jobs that install or start something this machine would have to set up). -
Proof on agent runs Iris did not write.
npm run proof -- --outsidejudges a fixed sample of the runs AgentDojo publishes, with the shipped engine at its defaults, and scores the verdicts against the labels AgentDojo's environment computed by checking the world after each run: was the user's task done, and did the attacker's goal happen. Every other proof line is measured on cases written or labelled here; this one is not. The sample is 700 runs of 29 agents and defences on all 97 of AgentDojo's user tasks, each file checked against the hash AgentDojo's repository lists for it, and CI regenerates the report on every change. What it found, at the shipped defaults:- The verdict shipped 312 of 419 bad runs and blocked 68 of 281 good ones.
- No injection rule caught any of the 200 runs where the attack succeeded. In every one the rule judged, it found no directive: AgentDojo's attack is worded in a way the directive list does not hold.
- Nearly half the good runs blocked were failed by
no_pii: an email assistant naming the addresses its user asked about, a banking assistant giving its own user an account number. The next most common wasno_silent_tool_failure.
Half the tasks are held out by a seed, so a rule changed after reading these runs is judged on runs it was not fitted to. The report, with every rate also counted over tasks, is
proof/OUTSIDE.md.
Changed
- Building Iris from a clone with pnpm 11 or later works again. pnpm 11 stopped reading its settings from
package.json, sopnpm installrefused the install scripts ofbetter-sqlite3andesbuildand stopped, and directories that build Iris that way, such as Glama, got no build. The allow-list is inpnpm-workspace.yamlagain, in the forms pnpm 10 and pnpm 11 and later each read. npm, which Iris itself installs with, is unaffected. - On Node 24.22.0 and later, a better-sqlite3 compiled on your machine loads as usual. Node 24.22.0 keeps the global list of addon cleanup hooks (nodejs/node#63985), so a binary compiled against Node 24.19+ headers no longer aborts when V8 frees one of its statements. Iris now loads that binary on 24.22.0 and later; on 24.19 to 24.21 it still holds the store with Node's built-in SQLite and says why. The CI job that compiles the addon from source on Node 24 confirmed it: the compiled binary survived collection on 24.22.0.
- The website and two blog posts give the EU AI Act's current dates. The agent-eval guide and the posts from March 2026 said the Act's requirements for high-risk systems took effect in August 2026, the date in the Act when they were written. An amendment in July 2026 moved them: they apply from 2 December 2027 for the systems listed in Annex III and from 2 August 2028 for those covered by Annex I. The guide now says so, and each post carries a dated correction.
- A shutdown that has to stop a storage thread says why.
close()gives the checkpoint thread and the search thread 5 s to end on their own. When one has not, Iris now writes one line to stderr before stopping it, naming the thread and what it was doing: still starting, in a request this process made (and for how long), or in its own periodic checkpoint. A shutdown that waited was silent before. - Links to the old roadmap resolve.
docs/roadmap.mdpoints at the capability map, which replaced it, so links to it in published posts and on other sites land somewhere current; the editor's notes on three posts link the map directly. - The homepage's research section leads with published research: the four-way comparison of Iris, Langfuse, Phoenix and Promptfoo, and each rule's measured precision and recall on /proof, in place of a survey card marked "Coming Soon". The terms add how a hosted or paid service would be offered: under its own terms, published before it starts, with no change to the open-source software's license.
- iris-eval.com counts page views with Vercel Web Analytics, as its privacy page describes: cookie-free, no personal data, no cross-site tracking. The script runs under the site's nonce-based Content Security Policy.
Security
- A tool call's arguments may hold at most 100,000 array elements and object keys, on every transport. The MCP SDK can count them before it validates the arguments, but only when the server sets a limit, and Iris set none (GHSA-46jr-4wx8-v453, high). Arguments that fail validation are reported element by element, so one 1 MB request of 524,229 elements (zeros where
spansbelong) cost the server 260 MB, and 20 requests of that size at once, the MCP endpoint's per-minute limit for one client, up to 1,148 MB and 41 s to answer them. A call over the limit now gets anisErrorresult that names the limit, before its arguments are validated, and the session goes on. The default clears every legitimate input measured, even one that fills the whole 1 MB request: real agent transcripts reach about 12,600 elements at 1 MB, and the densest shape found, a trace of 201 short tool calls, 68,506. Set it withsecurity.maxInputElementsinconfig.jsonorIRIS_MAX_INPUT_ELEMENTS; the same limit bounds the dashboard's request bodies (below). A server reachable beyond loopback can go lower: at 10,000 the same burst costs about 250 MB.scripts/bench-tool-input-elements.tsmeasures it. - Iris requires
@modelcontextprotocol/sdk1.32.0 or later. It required 1.30.0. A published package does not carry Iris's lockfile, so a project that already held SDK 1.30.x or 1.31.x kept it, and those versions are affected by GHSA-46jr-4wx8-v453, GHSA-22jm-h49p-29qw, GHSA-rvq5-wwqv-78pq and GHSA-6prh-2h8m-c8cw, and 1.30.x also by GHSA-6qxp-vccf-f47h and, at 1.30.0, GHSA-f897-62hf-6gxq. The element limit above also needs 1.32.0: an older SDK ignores the option. Iris's own lockfile already installed 1.32.0. ip-address10.7.4 (GHSA-rxp7-6hxx-x8q5, moderate:Address6strips a zone identifier in quadratic time before applying its length limit). Iris reaches it only throughexpress-rate-limit, which parses the request's own socket address: Iris leaves Express'strust proxyoff, so a client cannot hand it a long string. Theoverridesfloor is raised from 10.5.1 to 10.7.4.- With an allowlist set, the citation fetcher resolves only the hosts on it.
IRIS_CITATION_DOMAINSis now checked before the DNS lookup, so a host that an output cites and the list does not hold is refused withnot_allowed_domainand never looked up. It applies at every redirect hop. A host that is both outside the list and unsafe, such as a private address, now reportsnot_allowed_domainwhere it reportedssrf. - BREAKING — A tool argument can no longer widen what the operator allows for fetching or spend. Three arguments could.
verify_citationswithallow_fetch: truefetched the cited URLs on a server whose operator had not setIRIS_CITATION_ALLOW_FETCH=1; itsdomain_allowlistwas added to the operator'sIRIS_CITATION_DOMAINSinstead of narrowing it; andmax_cost_usdonevaluate_with_llm_judgeandmax_cost_usd_totalonverify_citationsreplaced the operator's cap with any larger number. An agent's arguments can be steered by text it read, including text stored in Iris, so each is now a ceiling: an argument can narrow the operator's setting for one call and never widen it. When one asks for more, the operator's setting applies and the call goes on, the response carries a warning with codeIRIS_ARGUMENT_NARROWEDnaming the argument and the setting that applied, and the stored evaluation keeps the same fact (provenance.narrowed) with a sentence to the operator in itsinterpretations, on every later read. Fetching now needsIRIS_CITATION_ALLOW_FETCH=1on the server; a caller that relied onallow_fetch: truegets no fetch and the warning. The per-call cap onverify_citationsis a new setting,IRIS_CITATION_MAX_COST_USD_TOTAL, default 1 USD as before. - BREAKING — Every judge call on your key draws on one daily budget.
evaluate_with_llm_judgeandverify_citationshad a per-call cap and no daily limit, so an agent calling either in a loop, or steered into doing so, could spend the key without end; only the relevance judge had a daily budget. All three now share one,IRIS_LLM_JUDGE_DAILY_BUDGET_USD(default 1 USD per UTC day, per tenant, kept in the database). A call is made only if its worst case fits in what is left. When it does not, nothing is spent:evaluate_with_llm_judgeanswersIRIS_BUDGET_EXCEEDED(retryable, with the time the budget resets), andverify_citationsmarks the citationdaily_budget_reachedand makes no further call. A deployment that spends more than 1 USD a day on the judge tools raises the budget. The variable wasIRIS_RELEVANCE_JUDGE_DAILY_BUDGET_USD; that name is still read when the new one is unset, and the server says so at startup. In the Claude Desktop extension the setting is now "LLM judge daily budget", and a value set under the old one is not carried over. Today's spend isjudge.dailyBudgetoniris://capabilities. - BREAKING — Stored text comes back fenced, and a page of traces carries snippets. Iris hands back what agents, their users and their tools wrote, and the agent reading it sits beside tools that delete traces and rules; a sentence planted in a trace reached the model as plain JSON. Every read that returns stored text now puts each value someone else wrote inside a tag carrying an id made for that response,
<untrusted_output id="…">…</untrusted_output id="…">, the fence Iris already used for its own judge, and the response starts withuntrusted: { id, notice }. The fence is on the values, so it reaches the model from the text block, fromstructuredContentand from a resource alike. A short value with no whitespace (an agent name, a session id, a tool name) is left as it is, so it still works as a filter. Covered:get_traces,list_rules,iris://traces/{trace_id},iris://evaluations/{id}(a custom rule's name and a judge's rationale; what Iris wrote stays outside),iris://auditandiris://dashboard/summary.get_tracescuts each fenced value to 500 characters unlessinclude_text: true, and says what it cut in each trace'scut.deploy_ruleandevaluate_output'scustom_rulesrefuse a value carrying a fence tag, anddeploy_ruletakes the rule names the dashboard always required (letters, digits, dot, dash, underscore).get_tracesandlist_rulesadvertiseopenWorldHint: true. A script that read a trace's text fromget_tracesreads it inside the tags, and passesinclude_text: truefor more than 500 characters. - Three dependency advisories, each patched within a day of GitHub publishing it; none affects Iris as shipped.
proxy-addr2.0.8 (GHSA-jqcg-44mw-7w3h, critical) fixes how a trusted proxy address range is matched; that matching runs only when Express'strust proxyis set to a range, and Iris leavestrust proxyoff.source-map-js1.2.2 (GHSA-68fv-2mgg-jv7q, high) is in the build and test tools only.@modelcontextprotocol/client2.3.1 (GHSA-6qxp-vccf-f47h, high) is in the OpenTelemetry example only. - Dashboard and OTLP request bodies are held to the same element limit. A JSON body sent to the dashboard API or to
POST /v1/tracesthat holds more thansecurity.maxInputElements(default 100,000) array elements and object keys is answered413before any schema reads it; JSON bodies of every legitimate shape measured stay under it at the full 1 MB request size. A protobuf body onPOST /v1/tracesis counted as it is decoded against its own ceiling, one element per 5 bytes ofsecurity.requestSizeLimit(209,716 at the default 1 MB) ormaxInputElementswhen that is higher. An OTLP exporter drops a batch it gets a413for, and decoded protobuf is denser than JSON: the densest real exporter body measured, from the plain Python exporter, holds one element per 7.5 bytes, so a batch that dense is taken up to the full request size with half again as much room to spare, and raisingrequestSizeLimitraises the ceiling with it. While the limit covered tool calls only it was namedmaxToolInputElements(IRIS_MAX_TOOL_INPUT_ELEMENTS); that name is still read when the new one is unset. - Validation errors return at most 20 issues and the total. A 400 from the dashboard API or the MCP HTTP transport for a body or query that fails validation still lists the issues in
details, the first 20 of them, and now carriesissueCount, how many there were, withdetailsNotesaying so when some were left out. An issue about unknown keys names at most 20 of them (keyCountgives how many), and a message is cut at 1,000 characters. The same bound holds for the reasonsPOST /v1/tracesgives for dropped spans (three issues for each span), forIRIS_INVALID_RULE_CONFIG, and for the unknown-argument message of every MCP tool. A request wrong in a few places is still told every one of them; the size of the answer no longer grows with the request. Twenty covers a trace, the largest body the API takes, with all 18 of its top-level fields wrong. body-parser2.4.0 andzod4.6.7 are now the lowest versions Iris accepts.body-parserbefore 2.4.0 (GHSA-27r3-jxmh-p828, high) could end the process when a compressed request body was followed by stray bytes. Iris decompresses request bodies on both HTTP servers, because OpenTelemetry exporters send gzip, and parses them before the API-key check.zodbefore 4.6.6 (GHSA-mrhx-c7xc-m3w5, high) collected one issue for every failing element of an array with no length limit. Iris validates every tool call and request body with it, and the element limit above already bounds how many one request can make it collect. A published package does not carry Iris's lockfile, so Iris now declaresbody-parserat^2.4.0(Express's own range admits 2.3.0) andzodat^4.6.7: a project that held an older version moves when it upgrades.fast-uri3.1.9 (GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr, moderate). Iris reaches it only throughajv'sformat: "uri"check, which Iris's own validator does not run. Theoverridesfloor is raised from 3.1.8.- The Docker image runs Node.js 24.22.1, the security release of 2026-10-20, on Alpine 3.24.3. It ran 24.21.0. The release fixes advisories in Node's
http,tls,dnsand brotli code, and carries undici 7.30.1 for Node's ownfetch, which the citation fetcher uses.
Supply-chain transparency
- SBOMs:
iris-npm-sbom.spdx.json+iris-docker-sbom.spdx.json(attached below). Both are SPDX 2.3 JSON, cover direct + transitive dependencies. - SBOM signatures: each SBOM has a companion
.sigstore.json(Sigstore bundle — signature + cert + Rekor entry) attached to this release. The release workflow runs this exact verification itself before it reports success. Verify with:cosign verify-blob \ --bundle iris-npm-sbom.spdx.json.sigstore.json \ --certificate-identity='https://github.com/iris-eval/mcp-server/.github/workflows/release.yml@refs/tags/v0.21.0' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \ iris-npm-sbom.spdx.json - npm provenance: published with
--provenance(verifiable vianpm audit signaturesor on the package page). Any npm library or launcher version this workflow publishes beside the server carries provenance too. - Docker signature: image signed with cosign keyless (Sigstore). Verify with:
cosign verify ghcr.io/iris-eval/mcp-server:v0.21.0 \ --certificate-identity='https://github.com/iris-eval/mcp-server/.github/workflows/release.yml@refs/tags/v0.21.0' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' - Build attestation: both the npm SBOM and Docker image manifest carry GitHub-signed build-provenance attestations. Inspect with
gh attestation verifyorcosign verify-attestation. - MCPB bundle:
iris-eval.mcpb(open it to install in Claude Desktop) is built from the npm tarball above plus the production dependencies in this tag'spackage-lock.json, and holds the tarball's files byte for byte. It is signed (iris-eval.mcpb.sigstore.json) and carries a build-provenance attestation. Verify with:cosign verify-blob \ --bundle iris-eval.mcpb.sigstore.json \ --certificate-identity='https://github.com/iris-eval/mcp-server/.github/workflows/release.yml@refs/tags/v0.21.0' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \ iris-eval.mcpb gh attestation verify iris-eval.mcpb -R iris-eval/mcp-server
What's Changed
- The research section leads with published research; the terms say how a paid service would be offered by @irparent in #810
- The search-worker equality test compares two completed searches by @irparent in #811
- Links to the old roadmap resolve: docs/roadmap.md points at the capability map by @irparent in #812
- A shutdown that has to stop a storage thread says why by @irparent in #814
- CI and Dependabot: Electron's download cached and retried, the judge run gets 45 minutes, real clients current, only landable updates proposed by @irparent in #813
- deps: this week's minor and patch updates, measured together by @irparent in #829
- deps: TypeScript 6.0 for the server, the SDK and the LangChain handler by @irparent in #830
- deps(dashboard): bump vitest from 4.1.11 to 5.0.3 in /dashboard by @dependabot[bot] in #818
- Worker-exit test: Iris's threads start with no --import, as an installed server's do by @irparent in #831
- Proof: Iris on agent runs it did not write (AgentDojo) by @irparent in #832
- npm run preflight: what CI will say, before you push by @irparent in #833
- Operator settings are ceilings a tool argument cannot widen by @irparent in #840
- A fast local preflight by default; test totals counted at a release by @irparent in #841
- One daily budget for every judge call on the user's key by @irparent in #842
- CI: a shorter critical path, a timeout on every job, one run per PR by @irparent in #845
- Stored text comes back fenced as untrusted by @irparent in #844
- Worker exit runs on main only when a merge touches what it tests by @irparent in #843
- Dependabot: one grouped PR per directory, after a five-day cooldown by @irparent in #846
- The required-checks guard reads rulesets as well as branch protection by @irparent in #847
- Pull requests are gated on the advisories they add; the inventory runs on main and daily by @irparent in #849
- The fast preflight runs the tests a change reaches, within a cap by @irparent in #850
- A release requires main's checks on its commit, runs its image, reads PyPI back by @irparent in #848
- Fixes to #845: e2e caches its browsers and gets 40 minutes; main's runs never replace each other by @irparent in #852
- deps: bump proxy-addr from 2.0.7 to 2.0.8 by @dependabot[bot] in #853
- deps(sdk): bump @types/node from 22.20.4 to 22.20.5 in /packages/sdk by @dependabot[bot] in #839
- deps: source-map-js 1.2.2 (GHSA-68fv-2mgg-jv7q); changelog records both patched advisories by @irparent in #858
- docs: no one can bypass the required checks, maintainers included by @irparent in #859
- preflight: the website type check starts without route types a local build left behind by @irparent in #860
- deps(examples): bump @modelcontextprotocol/client from 2.1.0 to 2.3.1 in /examples/otel-recipes/js by @dependabot[bot] in #854
- ci: Python releases pass the same gate as npm releases: main's checks on the tagged commit by @irparent in #861
- docs(changelog): the three advisories patched this cycle, each within a day of publication by @irparent in #862
- website: iris-eval.com counts page views with Vercel Web Analytics, as its privacy page describes by @irparent in #863
- ci: e2e installs the browsers' system libraries with apt timeouts and a 10-minute bound by @irparent in #864
- security: the citation fetcher checks the allowlist before any DNS lookup by @irparent in #865
- fix(build): check-tools-listed reports a server that exited early, and cleans up by @irparent in #866
- docs: the guide and two blog posts give the EU AI Act's current dates by @irparent in #867
- Green again: retention tests dated relative to the run, and Node 24.22.0's cleanup-hook fix recognised by @irparent in #889
- deps: bump hono from 4.13.7 to 4.13.13 by @dependabot[bot] in #888
- deps(dashboard): bump vite from 8.3.1 to 8.3.4 in /dashboard by @dependabot[bot] in #886
- deps: bump vite from 8.3.2 to 8.3.5 by @dependabot[bot] in #887
- deps(dashboard): bump storybook from 10.6.1 to 10.6.3 in /dashboard by @dependabot[bot] in #890
- Website: Next.js 16.4.2 by @irparent in #894
- Security: limit tool-call input elements, require MCP SDK 1.32.0, ip-address 10.7.4 by @irparent in #896
- Security: bound validation errors and count request bodies before validating them by @irparent in #897
- CI: native SQLite install check, cached apt packages for e2e, and a date-proof upgrade test by @irparent in #899
- Security: zod 4.6.7, body-parser 2.4.0, fast-uri 3.1.9, and the image on Node.js 24.22.1 by @irparent in #898
- Build from a clone with pnpm 11 and later, and this week's dependency updates by @irparent in #901
- release: 0.21.0 by @irparent in #910
Full Changelog: v0.20.0...v0.21.0