[0.13.0] - 2026-09-08
Found and fed. The engine, the proof and the release gate were already held to a higher standard than the doors that introduce the product. This release is the doors: one name on every surface, capture that does not depend on the model choosing to call a tool, the verdict's own reasons reaching every reader, and a container that cannot expose an unauthenticated eval API by accident.
The test for the release is the reviewer's, adopted verbatim: a stranger following the README gets a non-empty dashboard and a verdict with coverage that includes safety, without reading the blog — and, with the capture plugin, without asking the model to use Iris.
Check before upgrading. Three behaviours change; each is one sentence.
- A non-loopback bind without an API key is refused at startup.
--transport httpor--dashboardon0.0.0.0, a LAN address or a container now stops with one sentence namingIRIS_API_KEY; a baredocker runof the image stops too, anddocker compose uprequiresIRIS_API_KEY. Set the key, bind to127.0.0.1, or setIRIS_ALLOW_UNAUTHENTICATED=1to run open on purpose. Loopback without a key keeps working. - A threshold you set that equals the shipped default now gates. A deployment that deliberately set
cost_threshold: 0.10(the shipped number) was stamped "default" and demoted to advisory; it now gates as configured.max_stepsat the shipped default now advises, as every surface said it did. rolereportsgate,veto,riskoradvisory— the values the schema advertised since 0.9.0;termleaves the vocabulary.
Nothing in the measured verdict moves: the proof files regenerate with the version and commit only.
Added
-
A new command,
iris-eval ingest, stores and evaluates traces from stdin or a file and can fail a CI job on a named verdict basis. The third door, after the MCP tools andPOST /api/v1/traces, and the one that needs no server: one JSON trace or NDJSON in, one JSON line per trace out (trace_id,evaluation_id,passed,verdict.basis, what was not judged),--fail-on <basis|fail|unknown|any>for the exit code. The same ingest schema and the same store-and-evaluate primitive as the other two doors. It never sweeps retention. Recipe:docs/ci-gate.md. -
Two Iris processes can open one database for the first time at once. Migrations now take the write lock before they read (
BEGIN IMMEDIATE) and re-check the applied set inside it, so a server booting and a hook-driveningeston a fresh file both succeed; the loser used to fail onSQLITE_BUSY_SNAPSHOTor a duplicate column. -
An optional second plugin,
iris-eval-capture, records each Claude Code turn's prompt, tool calls and final answer into Iris without the model calling a tool, with critical spans redacted in the stored evaluation text. Three hooks (UserPromptSubmit,PostToolUse,Stop) assemble the turn and hand it toiris-eval ingest --evaluate --redact critical_spans --source hook, detached, so the turn never waits. It skips a turn the model logged itself (under either name Claude Code gives Iris's tools), filters Iris's own calls out of the trajectory, prints nothing (a Stop hook's stdout becomes model context), and is never bundled intoiris-eval: installing the eval plugin changes nothing about your turn loop. Named limits in its README: only the final assistant message of a turn is recorded, and the first run pays npx's cold start for the version the manifest pins. -
Every trace records the door it came through —
source:tool,http,cliorhook(migration 010) — so a host hook and a model-initiated log of the same turn can be told apart. -
log_tracecan evaluate in the same call.evaluate: true(withoutput, and optionallyeval_type) scores the stored trace under exactly the rulesevaluate_outputruns and returns the full evaluation — verdict, basis, every rule result, coverage — linked to the trace. It is the opt-inPOST /api/v1/traceshas carried since 0.5.0, and the MCP path lacked it: two calls where one would do taught agents to log and forget, and a trace with no verdict looks like a dead server. Both doors now share one store-and-evaluate primitive (src/eval/ingest.ts), so they cannot disagree about what "evaluate on write" means. Without an output, or on a server with no eval engine, the call is refused withIRIS_INVALID_ARGUMENTbefore anything is stored. -
The API reference's
log_tracetable gained thetools,runandcase_keyrows it had lacked since 0.11.0 and 0.12.0, and a test now reads the table against the tool's own input shape. -
The discovery manifest at
/.well-known/mcp.jsonis rendered from the built server. Tools with their one-sentence summaries, every resource and template, the prompt, and install blocks for Claude Code, Claude Desktop, Cursor and Docker, all keyediris-eval(npm run mcp-json:render;mcp-json:checkfails CI when the committed file differs). It was hand-maintained and listed one resource of five, no prompt and one install block. -
Paste-ready listing copy per directory — Glama, mcp.so, PulseMCP, Smithery, cursor.directory, awesome-mcp-servers — under
docs/launch/listings/, rendered from the truthbase likellms.txt, so a refresh is a paste and the numbers are the day's.
Fixed
- Every evaluation now carries
interpretations[]— the sentence that says why a rule that failed did not decide. The composer has built it since 0.10.0 (its own docblock called it mandatory), the engine attached it, and the serializer never emitted it, the schema had no field for it, and no read path carried it — so every reader sawcost_under_threshold: failedbesidepassed: trueand nothing else. It now rides the tool, the resource and both routes, and is derived on read from the composer facts the provenance now stores. A new note, addressed to the agent, names each question that was not judged and the input that would let it be. - BREAKING — A threshold you set that happens to equal the shipped default now gates. Three rules decided
thresholdSourceby comparing the value to the shipped number, so a deployment that deliberately setcost_threshold: 0.10was stamped "default" and demoted to advisory. The source now comes from the engine — which keys this call supplied, which the config file supplied — and never from value equality. max_stepsat the shipped default now advises, as every surface said it did. It read presence incustomConfig, which the engine defeats by merging the shipped thresholds into every call, so it gated at the default while its own message said it advised. Same fix.- BREAKING —
rolereportsgate,veto,riskoradvisory— the four values the schema has advertised since 0.9.0. The stamp could only producevetoorterm; a gating policy reportedterm. The engine now sets the role from the composer's own predicates, so the two cannot disagree, andtermleaves the vocabulary. - A stored evaluation reads back under the composer facts that wrote it. Reads re-composed the verdict under the shipped defaults; a deployment with its own
falsePassCostsaw one verdict on the tool and another on the dashboard.provenance.composernow carriesdefaultsGate,falsePassCostandonCriticalSkipped. coverage.questions[]carriesevaluatedofof: "judged" used to mean "at least one rule ran"; 1 of 3 now says so, and names what the others lacked.
Security
- BREAKING — Binding the HTTP transport or the dashboard to a non-loopback address without an API key is refused at startup — this includes a bare
docker runof the image, which binds0.0.0.0inside the container; setIRIS_ALLOW_UNAUTHENTICATED=1to run open on purpose.docker compose uprequiresIRIS_API_KEY. Both servers used to warn and serve: every trace, verdict and rule reachable by anyone who could route to the host, with the warning scrolling past in a container log. One policy (src/utils/bind-policy.ts) now decides for the CLI pre-flight and both server factories, so they cannot disagree. Loopback with no key keeps its warning. - The DNS-rebinding guard runs before the body parser on both servers. A request from a rejected Origin used to have up to the 1 MB request limit read and parsed before the 403; the rejection now comes first. The MCP transport gains the same guard middleware the dashboard has had since 0.6.0, ahead of the SDK's own check.
- The dashboard's session map no longer evicts a live session at its cap. The 257th sign-in used to drop the oldest session whether or not it was still valid, so a burst of sign-ins — or one holder of the key — silently logged every live browser out. Expired sessions are swept first; a sign-in that still finds every slot live is refused with
503and no cookie.
Changed
- The server's instructions and both skills now say WHEN to call Iris, not only what the tools do. After an answer the user will act on, after a task that used tools, and before saying a run was clean:
log_tracewithevaluate: true, then readverdict.basisandinterpretations. Not every line — the outputs that matter. The instructions ceiling moves from 2,600 to 3,000 characters for that paragraph. The skill's "How to Read a Result" table had still describedpassedasscore >= threshold; it now describes the composer, and the docs contract learned that spelling. - The config key, plugin name and skill name are
iris-evalon every surface, and the command isiris-eval. Until now the product answered to five names depending on the door — the repo's own.mcp.jsonsaidiris, the plugin manifestiris, the marketplaceiris-eval, the commandiris-mcp, the compose service and the OTel defaultiris-mcp— so an agent that copied one door could not match the docs of another, and the bare word lands on three other projects. One identifier now (src/identity.ts, locked bytests/identity.test.ts); "Iris" stays the product's name in prose. An entry you namediriskeeps working — the key is yours — andiris-mcpstill runs; neither is documented any more. The plugin's skill isiris-eval(it wasagent-eval); the defaultservice.nameon exported spans isiris-eval; the citation verifier's user agent no longer carries a version that stopped being true in 0.5. --self-testis the first line of every install snippet (README, the site, the skill,llms-full.txt): prove the install works on this machine before pasting a config block.- The LangChain example sends. It pointed at the MCP transport port, built a request and printed it; it now POSTs to
/api/v1/traceson the dashboard port withevaluate: trueand prints the verdict, the basis and what was not judged. - Retention is said where the user is already reading.
--self-testprints this install's policy (what is deleted, how often, and the two config keys that change it), andiris://capabilities/GET /api/v1/capabilitiescarry it asretention: { days, sweepIntervalHours }. Thirty days of silent deletion was a data-loss surprise documented only in the README's data section. smithery.yamlno longer offers anirisPortfield: the listing starts Iris over stdio, whereIRIS_PORTis never read.- The capability map re-read against what shipped. Seven cells of the multi-run column said "no grouping over repeated runs" after
compare_tracesshipped exactly that; they now say what the per-case pass rate over repeats answers and what it does not (the composed verdict, not one question). "Can this verdict be trusted" over repeats and over a population arehas(the Wilson interval per case; the drift and run comparisons with their intervals); "did it complete the task" for the evaluator itself ispartialonask_coverage's published family. The map contract now refuses agapcell that names a registered tool, rule, route or resource. /proofsays why τ stays at 0.50 when the dev-split sweep peaks at 0.20 — the class-prior arithmetic and one rule's positive predictive value computed from the proof files at render time, the direction read from the reliability bins — and whereconfidence: "marginal"is the conformal prediction set under another name.robots.txtnames/llms.txtand/llms-full.txtas allowed.npm run buildbuilds the dashboard, then the server, andnpm packrefuses a tarball without the dashboard. The build wastscalone;dist/dashboardexisted only because CI and the release ran a second step, so a localnpm packafternpm run buildshipped a server whose--dashboardserved nothing.prepacknow names what is missing; the CI build job reads npm's own pack list fordist/dashboard/index.html.pnpm-workspace.yamlis gone: it declared no workspace, only pnpm's allow-list for the two packages with install scripts, which now lives inpackage.jsonunderpnpm.onlyBuiltDependenciesfor harvesters that install with pnpm. npm is the one package manager.- The stranger harness grades its HTTP-route rows on the outcome — the must-not-ship outputs named with their rules, a verdict quoted with its basis — on whichever route the agent found (REST ingest, MCP over HTTP, the CLI), and its twelve-call ceiling carries the measurement it comes from.
scripts/verify-release.mjs(npm run verify:release) reads F1–F6 from outside — npm, GHCR, the GitHub release, the registry, the live site, a freshnpx … --self-test— so a release record quotes an instrument instead of restating the workflow by hand.- The discovery sentence on every surface names the three ways a trace reaches Iris: the agent calls a tool, a host hook or
iris-eval ingesthands it one, or you POST it to the HTTP API.
Verify: npm run verify:release -- --version 0.13.0 (F1–F6 from outside), npm run mcp-json:check, npm run llms:check, npm test; the stranger record for this release is linked from the README of tests/acceptance/stranger/.
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-regexp='https://github.com/iris-eval/mcp-server' \ --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). - Docker signature: image signed with cosign keyless (Sigstore). Verify with:
cosign verify ghcr.io/iris-eval/mcp-server:v0.13.0 \ --certificate-identity-regexp='https://github.com/iris-eval/mcp-server' \ --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.
What's Changed
- feat(identity): iris-eval is the one identifier (A6-1) by @irparent in #503
- feat(tools): log_trace can evaluate in the same call (A6-2) by @irparent in #504
- fix(eval): the stamp tells the truth — interpretations emitted, role from the composer, thresholdSource from provenance (A6-3) by @irparent in #505
- feat(cli): iris-eval ingest — the third door, a CI gate, and lock-first migrations (A6-4) by @irparent in #506
- feat(capture): iris-eval-capture — every Claude Code turn into Iris without the model calling a tool (A6-5) by @irparent in #507
- feat(instructions): the loop as a policy — WHEN to call Iris (A6-6) by @irparent in #508
- fix(security): refuse, don't warn — a non-loopback bind without a key stops at boot (A6-7) by @irparent in #509
- feat(claims): render what was hand-maintained — the manifest, the listings, the map re-read (A6-8) by @irparent in #510
- fix(build): one build carries the dashboard; the harness grades outcomes; F1–F6 gets an instrument (A6-9) by @irparent in #511
- test(stranger): the two capture phases — V1 hook alone, V2 hook beside iris-eval (A6-11 prep) by @irparent in #512
- chore(release): 0.13.0 — Found and fed by @irparent in #513
- fix(release): validate installs the dashboard before the one build; a lock enumerates every root build by @irparent in #514
Full Changelog: v0.12.1...v0.13.0