Skip to content

v0.13.0

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Sep 07:24
· 4 commits to main since this release
f0abd75

[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 http or --dashboard on 0.0.0.0, a LAN address or a container now stops with one sentence naming IRIS_API_KEY; a bare docker run of the image stops too, and docker compose up requires IRIS_API_KEY. Set the key, bind to 127.0.0.1, or set IRIS_ALLOW_UNAUTHENTICATED=1 to 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_steps at the shipped default now advises, as every surface said it did.
  • role reports gate, veto, risk or advisory — the values the schema advertised since 0.9.0; term leaves 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 and POST /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-driven ingest on a fresh file both succeed; the loser used to fail on SQLITE_BUSY_SNAPSHOT or 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 to iris-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 into iris-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, cli or hook (migration 010) — so a host hook and a model-initiated log of the same turn can be told apart.

  • log_trace can evaluate in the same call. evaluate: true (with output, and optionally eval_type) scores the stored trace under exactly the rules evaluate_output runs and returns the full evaluation — verdict, basis, every rule result, coverage — linked to the trace. It is the opt-in POST /api/v1/traces has 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 with IRIS_INVALID_ARGUMENT before anything is stored.

  • The API reference's log_trace table gained the tools, run and case_key rows 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.json is 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 keyed iris-eval (npm run mcp-json:render; mcp-json:check fails 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 like llms.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 saw cost_under_threshold: failed beside passed: true and 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 thresholdSource by comparing the value to the shipped number, so a deployment that deliberately set cost_threshold: 0.10 was 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_steps at the shipped default now advises, as every surface said it did. It read presence in customConfig, 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 — role reports gate, veto, risk or advisory — the four values the schema has advertised since 0.9.0. The stamp could only produce veto or term; a gating policy reported term. The engine now sets the role from the composer's own predicates, so the two cannot disagree, and term leaves 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 falsePassCost saw one verdict on the tool and another on the dashboard. provenance.composer now carries defaultsGate, falsePassCost and onCriticalSkipped.
  • coverage.questions[] carries evaluated of of: "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 run of the image, which binds 0.0.0.0 inside the container; set IRIS_ALLOW_UNAUTHENTICATED=1 to run open on purpose. docker compose up requires IRIS_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 503 and 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_trace with evaluate: true, then read verdict.basis and interpretations. 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 described passed as score >= threshold; it now describes the composer, and the docs contract learned that spelling.
  • The config key, plugin name and skill name are iris-eval on every surface, and the command is iris-eval. Until now the product answered to five names depending on the door — the repo's own .mcp.json said iris, the plugin manifest iris, the marketplace iris-eval, the command iris-mcp, the compose service and the OTel default iris-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 by tests/identity.test.ts); "Iris" stays the product's name in prose. An entry you named iris keeps working — the key is yours — and iris-mcp still runs; neither is documented any more. The plugin's skill is iris-eval (it was agent-eval); the default service.name on exported spans is iris-eval; the citation verifier's user agent no longer carries a version that stopped being true in 0.5.
  • --self-test is 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/traces on the dashboard port with evaluate: true and prints the verdict, the basis and what was not judged.
  • Retention is said where the user is already reading. --self-test prints this install's policy (what is deleted, how often, and the two config keys that change it), and iris://capabilities / GET /api/v1/capabilities carry it as retention: { days, sweepIntervalHours }. Thirty days of silent deletion was a data-loss surprise documented only in the README's data section.
  • smithery.yaml no longer offers an irisPort field: the listing starts Iris over stdio, where IRIS_PORT is 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_traces shipped 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 are has (the Wilson interval per case; the drift and run comparisons with their intervals); "did it complete the task" for the evaluator itself is partial on ask_coverage's published family. The map contract now refuses a gap cell that names a registered tool, rule, route or resource.
  • /proof says 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 where confidence: "marginal" is the conformal prediction set under another name.
  • robots.txt names /llms.txt and /llms-full.txt as allowed.
  • npm run build builds the dashboard, then the server, and npm pack refuses a tarball without the dashboard. The build was tsc alone; dist/dashboard existed only because CI and the release ran a second step, so a local npm pack after npm run build shipped a server whose --dashboard served nothing. prepack now names what is missing; the CI build job reads npm's own pack list for dist/dashboard/index.html.
  • pnpm-workspace.yaml is gone: it declared no workspace, only pnpm's allow-list for the two packages with install scripts, which now lives in package.json under pnpm.onlyBuiltDependencies for 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 fresh npx … --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 ingest hands 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 via npm audit signatures or 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 verify or cosign 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