Changelog
All notable changes to Attest are documented in this file. The format follows Keep a Changelog, and the project follows Semantic Versioning. One VERSION covers the engine, the Python SDK (attest-ai) and the TypeScript SDKs (@attest-ai/core, @attest-ai/vitest); PyPI shows pre-releases in PEP 440 form (1.0.0-rc.1 → 1.0.0rc1).
Unreleased
1.1.1 — 2026-11-11
Documentation patch on 1.1.0 “Hardened”. It ships the refreshed guides, package READMEs and agent skills to PyPI, npm, attest guide, attest init --skills and the website, and moves the attest init --ci github annotate pin to v1.1.0. No change to evaluation, replay or policy behavior.
Changed
attest init --ci github(Python and TypeScript) pinspr-annotateto the v1.1.0 commit and fetchesattest-ai==1.1.0in the annotate job, instead of v1.0.1 (#254).@attest-ai/vitestrequires@attest-ai/core >=1.1.0as its peer, instead of>=1.1.0-rc.
Docs
- README: 1.1 status, a 30-second overview, every capability with its guide, a release history in place of the stale roadmap, the
attest doctorwarm-up before the first Vitest run, and a current banner (#256). - PyPI and npm READMEs:
uvinstall commands, minimal examples that evaluate their chain, supported Python and Vitest ranges (#256). - Guides: commands, flags, environment variables and exit codes checked against 1.1.0; plain-language openings and minimal examples where a guide had none; 1.1 behavior documented where it belongs (fail-closed judges and budgets, relational effects,
tests_failed, semantic refresh, per-operator mutation scores);uvinstead ofpipthroughout (#255, #257). - Agent skills: the capture skill describes the real-code replay test that capture scaffolds, not a frozen-fixture test (#256).
- Protocol spec: describes protocol v1 as shipped (stdio JSON-RPC only, no planned gRPC mode), with
initializeexamples checked against the engine (#258). pyproject.toml: theattest.pluginsentry-point example names a plugin instance, which is what the loader expects, not a class.
1.1.0 — 2026-11-11
Stable release themed “Hardened”. 1.1.0 is 1.1.0-rc.5 unchanged, after three consecutive green nightly walkthroughs of every refund-desk incident on the released channel. The release candidates added each piece: fail-closed judges, stable assertion ids and failing unevaluated chains (rc.1), runner parity on Vitest 3 and 4 and safe capture defaults (rc.2), a SHA-pinned supply chain, least-privilege pull-request annotation and Vitest 5 support (rc.3), relational world-effect contracts, semantic refresh diffs, per-operator mutation scores and executable public claims (rc.4), checked-chain ownership and failed-test policy gates (rc.5). No changes since rc.5.
Upgrade note (from 1.0.1): Several changes turn a silent pass into a failure. Read the Behavior change lines in the rc.1 to rc.5 entries below before upgrading; the rc.5 upgrade note covers chain re-evaluation, unscoped concurrent lifecycle assertions on Vitest <4.1 and the default tests_failed policy rule.
1.1.0-rc.5 — 2026-11-07
Fifth release candidate of 1.1.0, themed “Every check ran”. Checked-chain ownership and failed-test policy gates close silent-green paths in Python and Vitest, while packaged npm guides and provider-object handling improve onboarding and compatibility. Changes that alter results are marked Behavior change below.
Upgrade note: Chains are now enforced after append-after-evaluate, when fixture-built, and in isolated Vitest it.concurrent bodies: evaluate every chain again after adding assertions. Move unscoped concurrent lifecycle assertions into the test body or upgrade to Vitest >=4.1; cached, non-isolated concurrent files fail explicitly when guard hooks are unavailable. Policy evaluation now blocks failed native tests by default through tests_failed; explicitly configure tests_failed: warn or tests_failed: off only when a non-blocking verdict is intended.
Changed
- Behavior change: policy evaluation now blocks reports containing failed test outcomes, even when every Attest assertion passed; configure
tests_failed: warnortests_failed: offto explicitly downgrade or disable this rule. - Behavior change: policy evaluation exits 3 for report-v2 input with missing/null
testsor an outcome outsidepassed|failed|skipped, instead of treating the malformed input as a passing verdict.
Fixed
- Fix LlamaIndex trace construction for genuine OpenAI provider tool-call objects while retaining support for dictionary tool calls.
- Include installation instructions, a runnable Vitest example, and documentation links in the
@attest-ai/coreand@attest-ai/vitestnpm packages. - Behavior change: Python tests now fail with the existing
expect() chain built but never evaluateddiagnostic when assertions were added after the last evaluation. - Behavior change: Assertion chains created during fixture setup or teardown are checked for evaluation and attributed to the item executing that fixture phase rather than silently passing or leaking into the next test.
- Preserve scheme-2 assertion ids across reevaluation while assigning new assertions the next occurrence index in each scope/trial.
- Behavior change: Isolated Vitest concurrent test bodies now fail when their own Attest chains were never evaluated; another test’s evaluated chain cannot satisfy their guard. Unhooked concurrent chains in cached, non-isolated files fail explicitly instead of silently opting out.
- Behavior change: Adding an assertion after evaluation, including while an evaluation is in flight, leaves the chain unevaluated and fails teardown unless it is evaluated again.
- Behavior change: Unscoped concurrent lifecycle assertions on Vitest <4.1 fail with guidance instead of silently opting out. Move the chain into the body or upgrade Vitest.
- Preserve assertion ids across re-evaluation.
- Behavior change: Detect unevaluated mutations of chains shared across tests; untouched shared chains are not charged to another test.
- Behavior change: report Vitest suite/module
beforeAllandafterAllerrors as failures of their collected descendant tests instead of silently reporting them passed or skipped, so thetests_failedpolicy rule can block the run.
Docs
- Explain chain re-evaluation, pytest fixture ownership and shared mutations, concurrent Vitest guard coverage and compatibility limits.
- Document failed-test policy gating, invalid-report errors, and Vitest suite/module hook failure reporting.
Dependencies
- Python
google-genai1.63.0 → 1.75.0 (development/conformance only; the proposed 2.x update is incompatible with the framework test environment); user-facing optional ranges are unchanged. (#222) - TypeScript
vitest4.1.12 → 5.1.0 (development only); CI retains Node 20 consumer coverage with an isolated Vitest 4 override and tests the committed Vitest 5 default on Node 22; published Node/Vitest peer ranges are unchanged. (#223) - Python
openai1.83.0 → 2.54.0 (development/conformance only), with compatible CrewAI 1.10.1, Instructor 1.13.0 and LlamaIndex OpenAI 0.6.8 lockfile versions; user-facing optional ranges are unchanged. (#224) - Python
ollama0.6.1 → 0.6.3 (development/conformance only); user-facing optional ranges are unchanged. (#225) - Python
anthropic0.81.0 → 2.4.0 (development/conformance only), with development-lock OpenTelemetry security overrides; user-facing optional ranges are unchanged. (#226) - TypeScript
@langchain/core1.1.40 → 1.3.3 (development/conformance only); user-facing optional peer ranges are unchanged. (#227) - Python
langchain-core1.3.3 → 1.7.4 (development/conformance only); user-facing optional ranges are unchanged. (#228) - GitHub Actions development/CI tooling:
actions/checkoutv7.0.1,actions/setup-gov7.0.0,actions/setup-pythonv7.1.0 andactions/setup-nodev7.1.0 (major 7),astral-sh/setup-uvv10.3.3 (major 10), andpnpm/action-setupv6.1.1 (major 6); synchronize SHA-pinned generated Python/TypeScript workflows to checkout v7.0.1 and setup-uv v10.3.3, plus setup-node v7.1.0 for TypeScript. (#229)
1.1.0-rc.4 — 2026-11-07
Fourth release candidate of 1.1.0, themed “Evidence”. Relational world-effect contracts, semantic refresh diffs, per-operator mutation scores and executable public claims make the evidence behind a green gate visible. Changes that alter results are marked Behavior change below.
Upgrade note: Re-pin baselines containing effect assertions when upgrading from 1.1.0-rc.3 or earlier. Baseline comparison exits 3 when either a baseline or report contains legacy effect identities without effect_world_excluded: true; re-pin the baseline and re-run reports before comparing. The assertion id scheme remains 2.
Added
-
attest capture --refreshprints a per-exchange semantic diff (system prompt lines, tool schemas, messages, tool results, model, params) and reports “no semantic change” when nothing differs.--jsonresults carry adiffobject. -
Effect checks
sumandmatchesrelate two world resources: a refunded total stays within the order amount, a notification goes to the order’s customer. The contract DSL gainssum(...),matches(...)and therelated(resource, field, on=key)literal, anddraft_contractdrafts them from a world. -
TypeScript SDK:
sum()andmatches()world-effect checks and arelated()helper for relational effects. -
Python SDK:
sumandmatchesworld-effect checks and therelated()helper for relational effects (refunded total within the order amount; notification goes to the order’s customer). -
attest mutate --jsonreportsoperators(per-operator generated, killed, survived, no-applicable and score) andexcluded(skipped specs by reason, mutants without an applicable spec); the text table prints the same rows, andattest viewrenders them. -
alter_world_effectraises a numeric field above its related record’s value and redirects a shared*_idfield, sosumandmatcheseffect specs can kill world mutants and survivors suggest them. -
attest-engine capabilities [--json]prints the layers, assertion types, SDK languages, adapters and features with maturity tiers, the licence and versions; schemaproto/attest/v1/cli/capabilities.schema.json. -
make generate-claimsandmake check-claimsrender and verify the README layer table, adapter list and feature maturity table and the adapter guide’s tier table fromcapabilities --json. -
Issue templates “Report a regression attest missed” and “Design partner”.
-
related()is exported from both SDK roots (attest,@attest-ai/core). -
Guides for relational
sum/matcheseffects, relational drafting through MCP, the--refreshsemantic diff and the per-operator mutation report. -
CI
claimsjob runsmake check-claims, and CIsnippetsjob runs every runnable README and guide code block offline; both are required by therequiredgate. -
scripts/run_doc_snippets.py: snippet runner with<!-- snippet: ... -->markers; every skip and fragment carries a reason and is counted. -
attest capturedrafts relationalsum/matcheseffect contracts from the captured world and verifies them against it; with--expect-failthey are recorded as expected failures. A capped draft list is reported asrelational_cappedin the--jsonresult and in the summary.fixture-v1gains therelationalkind andeffecttype.
Changed
-
countwith a fractional orrelatedat_mostnow fails withinvalid effect spec: count: at_most must be an integerinstead of a JSON decode error. -
The
attest mutatetext table columns are nowoperator generated killed survived n/a score(wasoperator mutants killed survived). -
Behavior change: a world number whose decimal exponent is outside ±400 is now an evidence error (
effect evidence: … decimal exponent outside ±400) instead of being evaluated; relational sums print exact values up to 1024 digits and a numerator/denominator form beyond that. -
Public docs describe six evaluated layers plus plugins; the internal plugin layer id 8 is unchanged. Multi-agent assertions are documented as layer 3.
-
The README lists the 10 Python adapter modules that exist (it listed 11 including a non-existent Simulation adapter) and a maturity tier per adapter and feature.
Behavior change: a wrong method name, a stale import or an unmarked non-runnable code block in the README or a guide now fails CI; before, it passed silently.
Behavior change: a world capture without --expect-fail whose world violates a drafted relational contract is now not_verified (exit 1); before, relational contracts were not drafted and the capture passed.
Fixed
-
Behavior change: world numbers with a decimal exponent outside ±400 fail as effect evidence errors instead of passing.
-
@attest-ai/coreand@attest-ai/vitestdeclareApache-2.0(they declared MIT) and ship theLICENSEfile. -
Removed the README claims of a Go SDK, of SDK/engine release decoupling (the SDK requires the exact engine version) and of “70% of your agent’s testable surface is deterministic” (no source).
-
docs/guides/capture.mdfixture table listsfixture_versionandname; theprotocol-spec.mdhandshake example shows the current version. -
Behavior change: effect assertion ids and spec hashes exclude their runtime world snapshot, so a baselined passing effect contract that fails on changed world state is blocked by
block_on_regressionrather than silently classified as a spec edit. Re-pin baselines containing effect assertions when upgrading from 1.1.0-rc.3 or earlier; id scheme stays 2. -
Behavior change: baseline comparison exits 3 with a re-pin/re-run hint when either side contains legacy effect identities without
effect_world_excluded: true, preventing upgrade-time fail-open policy results. Non-effect-only legacy comparisons remain compatible. -
Require assertion-type context in low-level canonical-spec helpers and preserve non-object effect specs during identity encoding.
1.1.0-rc.3 — 2026-11-06
Third release candidate of 1.1.0, themed “Trusted supply chain”. Every GitHub Actions reference in the repository and in the attest init --ci github scaffold is pinned to a commit SHA and checked in CI, the scaffolded workflow gives pull-request write access only to a separate annotate job, and pr-annotate reports every report’s policy result. @attest-ai/vitest also loads on Vitest 5. Changes that alter results are marked Behavior change below.
Changed
- Every GitHub Actions
uses:in this repository’s workflows and composite actions is pinned to a commit SHA with a version comment; Dependabot’sgithub-actionsecosystem now covers the workflows and both composite actions, grouped.scripts/check-action-pins.pyruns in theGitHub configCI job. attest init --ci github(Python and TypeScript) writes a workflow with SHA-pinned actions and two jobs: the test job holdscontents: read, and a separateannotatejob holdspull-requests: write. The annotate step pins thepr-annotatev1.0.1commit instead of@v<sdk version>.- Behavior change: a workflow or action that references an action by tag now fails the
GitHub configcheck.
Fixed
pr-annotate: with several reports the policy line now shows the worst exit, every report’s violations (each prefixed with its report) and a per-report list instead of only the worst report.- CI: removed the
notify-docs-pythonandnotify-docs-typescriptjobs, which passed an emptyDOCS_DEPLOY_TOKENon push to main; the release workflow dispatches the website sync from thewebsite-syncenvironment. @attest-ai/vitestnow supports Vitest 5 (peer>=3.0.0 <6); it previously failed to load there.
1.1.0-rc.2 — 2026-11-06
Second release candidate of 1.1.0, themed “Real adapters, real parity”. The runner integrations behave the same on every supported version: @attest-ai/vitest loads and reports cost on Vitest 3 and 4, and the Python helpers keep trace metadata, accept plain agents and fail an offline run in one line. Safe defaults landed early: shipped skills no longer auto-confirm a recorded command, --attest-egress=block covers provider calls through HTTP proxies, and attest capture templates session identifiers and redacts every server-sent event. Changes that alter results are marked Behavior change below.
Changed
- Guides document the
@attest-ai/vitest/reporterand/setupsubpaths for Vitest 3 and 4, the removal ofAttestCostReporter, the Pythonset_metadatamerge, string-returning@agent, and the one-line offline engine failure. - CI runs the
@attest-ai/vitestsuite on the newest Vitest 3.x and 4.x and runspnpm lintfor the TypeScript SDK; both are required checks. - Behavior change: a TypeScript lint finding or a failure on either Vitest major now fails the
requiredcheck.
Fixed
AttestReporterand@attest-ai/vitest/setupload on Vitest 3 (they importedvitestat runtime, failing with “Vitest failed to access its internal state”); the peer range>=3.0.0 <5is now exercised on both majors.@attest-ai/vitest/setupexportssetupandteardown, soglobalSetup: ["@attest-ai/vitest/setup"]works and downloads the engine once before tests run.- The unevaluated-
expect()-chain guard now fails the right test on Vitest before 4.1. pnpm lintinsdks/typescriptruns Biome with a committed config; it checked nothing before.TraceBuilder.set_metadatamerges fields across calls instead of replacing the metadata.@agentwraps a string return as{"message": ...}, accepts positional arguments and honorsadapter=for provider adapters.- A failed engine download ends the run with one line naming
ATTEST_ENGINE_PATH; the download notice prints outside pytest capture. - Behavior change: a cost or latency breach set through separate
set_metadatacalls now fails instead of skipping asmissing_evidence. - Behavior change:
@agent(adapter=...)with a non-provider, non-manual adapter raisesTypeErrorat decoration instead of being ignored. - A TypeScript engine that exits during startup now fails with its stderr:
EngineManager.launchwaits (bounded, 1 s) for the exit report on stdout EOF before terminating the child.
Security
- Shipped skills no longer tell agents to pass
--yestoattest capture --refresh. They show the recordedsource.commandwith its SHA-256 and stop for human confirmation. - Behavior change:
--attest-egress=block/ATTEST_EGRESS=blocknow also refuses provider calls made throughHTTP(S)_PROXY(CONNECT and absolute-URI targets), in both SDKs; calls that previously passed through a proxy now fail withEgressBlockedError. - The egress guard’s provider host list now includes AWS Bedrock, Vertex AI, Mistral, Groq, OpenRouter and Together.
- Documented limit: child processes, SOCKS proxies and TLS-wrapped proxy connections are not covered.
- Behavior change:
attest captureandattest capture --check-redactionrefuse a.envthat is not a regular file or is over 1 MiB;attest replay(and--serve) warns once on stderr and ignores it. - Behavior change: a replay recorded before this change computes a different key for requests that carry
metadata.user_idor Claude Code environment reminders; keys are recomputed on load, so existing fixtures still replay, andattest capture --refreshrewrites them with templated identifiers. attest capturetemplates Claude Codemetadata.user_id(device, account and session ids) and leaves it out of the replay key, and normalizes environment<system-reminder>blocks for matching, so a replay in a new session hits.attest capturedecodes and redacts every server-sent event, so a secret containing a quote or backslash in a non-delta event such asresponse.completedno longer reaches the fixture;attest capture --check-redactionflags it without printing it.- Redaction covers JWTs, Azure, Mistral and Cohere keys introduced by
api-key,subscription-key,access-keyorclient_secret, those names as query parameters and headers, and the secret values in the working directory’s.env. - Replay-key normalization of Claude Code environment values is limited to Claude Code’s own sections (
# Environment,<env>,# currentDate,gitStatus:) and covers the indented entries underAdditional working directories:;Status:and similar lines in an application’s own prompt keep their meaning. - Redaction matches secrets escaped twice (JSON arguments inside raw JSON text), and a
.envvalue with an unterminated quote no longer hides the variables after it.
Removed
- Behavior change:
AttestCostReporteris no longer exported from@attest-ai/vitest. It printed nothing on Vitest 4. UseAttestReporter(reporters: ["default", "@attest-ai/vitest/reporter"]): it prints theattest usage:line from per-test meta and writes the cost to the report-v2 file.
1.1.0-rc.1 — 2026-11-06
First release candidate of 1.1.0, themed “Fail closed”. Judges are treated as measuring instruments: a malformed reply or a rejected credential is not_evaluated, never a pass, and only transient failures are retried. Green now means checked: assertion ids are stable (scheme 2), a removed assertion blocks the policy, and a test with an unevaluated expect() chain fails. The engine exits on signals and refuses invalid configuration, dynamic thresholds and continuous-eval drift alerts work, adapters conform to a shared behavioral corpus, and the SDK packages carry signed engine hashes. Several changes turn a silent pass into a failure; they are marked Behavior change below.
Upgrading from 1.0.x
- Re-pin baselines: assertion ids are scheme 2 (
a2_+ 32 hex), andattest policy evaluate --baselineexits 3 for a baseline pinned on id scheme 1. - Set
ATTEST_ENGINE_TIMEOUTin seconds for the TypeScript SDK (it was milliseconds; values above 3600 are refused). - History ids change, so drift baselines rebuild over the first runs after upgrading.
- Source and editable installs no longer download an engine: build one with
make engineand pointATTEST_ENGINE_PATHat it. ContinuousEvalRunnerrequires a name (name=in Python,options.namein TypeScript), and the TypeScriptstop()returns aPromise<void>.- Tests that never evaluate an
expect()chain now fail; evaluate the chain or remove it. - Tests that asserted the old TypeScript OpenAI adapter shapes, the CrewAI
agent_call/tool_callsteps or an empty output on an OpenAI refusal must be updated.
Added
failure_class: spec_errorfor invalid judge specs, andinitializecapabilitiesjudge_configuredandembedding_configured.judge_malformedandprovider_authnot_evaluated reasons in the report-v2 and verdict schemas, protocol fixtures and both SDKs’ constants (REASON_JUDGE_MALFORMED,REASON_PROVIDER_AUTH,FAILURE_CLASS_SPEC_ERROR/FailureClass.SpecError);attest explain_failure(MCP) names the next step for both reasons.- Report v2 fields
id_scheme,budget_exceeded, and per-rowscope_idandspec_hash; policy rulesblock_on_removalandallow_removed; a plugins guide. attest-engine replay --trace-out <file>writes the trace of the new run;replayRun(@attest-ai/core) andattest.capture.replay_runre-run a fixture’s source command under strict replay and return the new trace.- Continuous-eval guide covering runner setup in Python and TypeScript, the webhook and Slack alert payload, triggers, error handling, stop and drain, and the 10-run cold start. Documentation for
threshold="dynamic"onpasses_judgeandoutput_similar_to. AttestClient.queryDrift(TypeScript) andAttestClient.query_drift(Python) accept a static threshold.- Python
ContinuousEvalRunnertakes anon_errorcallback.
Changed
- Behavior change: CI now fails when a framework-gated adapter test is skipped because its framework or the engine is missing; previously these skipped silently.
- Behavior change: CrewAIAdapter traces are summary-only (output, tokens, latency). Agents and tasks from the crew’s configuration are no longer recorded as
agent_callandtool_callsteps, so assertions on those steps now fail. - Behavior change:
ContinuousEvalRunner.stop()returns aPromise<void>and waits (bounded) for in-flight evaluations and alert dispatches. - Behavior change:
ContinuousEvalRunnerrequires aname=keyword, the scope of its assertion ids. - Behavior change:
ContinuousEvalRunnerrequiresoptions.name. - Guides document the judge reply contract, per-call nonce delimiters, selective retry (408/429/5xx/network; 401/403 is one call), the ACP environment allowlist,
ATTEST_ENGINE_TIMEOUTin seconds in both SDKs, the new initialize capabilities and the 1.1.0 JSON-RPC and numeric-environment behavior. - CI lints the engine with golangci-lint and tests the Python SDK on 3.13 and 3.14, and runs the adapter tests against real crewai, langchain-core, google-adk, llama-index-core and provider SDKs from
uv.lock. Thecrewaiextra is not installed on Python 3.14 (its dependency chain pins a pydantic-core release without 3.14 wheels). attest captureoutput, drafted tests and the capture guide say “consistent with the recording” instead of “verified”, and state that replay keeps relative, not absolute, identity.- Corrected
results[].drift.countin the protocol spec to the number of baseline rows.
Fixed
- Behavior change: a judge reply with a missing, non-numeric or out-of-range
scoreis nownot_evaluated(judge_malformed); it was a measured pass ({"score":7}) or a measured 0 hard fail. - Behavior change: provider 401/403 responses are no longer retried; the assertion is
not_evaluated(provider_auth)after one judge call (previously four calls, 3.5 s,provider_unavailable). Other non-transient errors (400, 404, unparseable replies) are also no longer retried. - LLM provider retries use jittered backoff and honour
Retry-Afteron 408/429/5xx. - Judge prompts delimit agent output with a per-call random nonce; output containing the old fixed delimiter cannot close the data block, and a judge reply containing the nonce is
judge_malformed. - The judge cache is consulted only after the spec validates, and its key includes
repeatand bias probes. failure_classfor judge failures is computed against the assertion’s own threshold, so a 0.78 score against 0.8 isstochastic_variancerather thanbroken_code.- The ACP judge agent starts with an allowlisted environment (process basics, locale, Windows system variables, proxy/CA settings,
CLAUDE_CODE_OAUTH_TOKEN,CLAUDE_CONFIG_DIR,CODEX_HOME) instead of inheriting everything but ten variables, so unrelated credentials such asGITHUB_TOKENandAWS_SECRET_ACCESS_KEYno longer reach it. - Behavior change: ACP agents that relied on other inherited variables (for example
XDG_*,NPM_CONFIG_*) no longer see them. - Behavior change: a test that builds an
expect()chain and never evaluates it now fails with “N expect() chain(s) built but never evaluated”; evaluating a chain with no assertions fails. Both used to pass. - Behavior change: a Vitest test that builds an
expect()chain and never passes it toevaluate()/inspect(), or evaluates a chain with no assertions, now fails instead of passing. - Behavior change: assertion ids are now
a2_plus 32 hex characters over the test node id (orContinuousEvalRunnername), assertion type, canonical spec and occurrence index. Reordering assertions keeps their ids, identical assertions get distinct ids, and editing a spec makes a new id. Baselines pinned on 1.0.x must be re-pinned. - Both SDKs derive assertion ids this way.
- Behavior change:
attest policy evaluate --baseline <tag>exits 1 when a baselined assertion id is missing from the report (block_on_removal, on by default with a baseline). List accepted removals underallow_removed; a spec edit is reported aschangedand does not block. - Behavior change:
attest policy evaluate --baselineexits 3 withbaseline uses id scheme 1; re-pin it on 1.1for baselines pinned before id scheme 2. - Behavior change: a run that exceeded
ATTEST_MAX_SOFT_FAILURESblocks the policy (budget_exceeded);evaluate_batchnow returns every result with abudget_exceededflag instead ofENGINE_ERROR, so hard failures are no longer dropped. Reports carry the flag, true when any batch pushed the session past the limit. .plugin()assertions run their registered plugin and submit the result before evaluation, so the row follows the plugin result and judge and embedding assertions in the same batch still evaluate (Python: register plugins onattest_engine.plugin_registryor through theattest.pluginsentry point; TypeScript:attestPlugins).- Behavior change: the engine exits 0 within about a second of SIGTERM or SIGINT, cancelling an in-flight judge call, instead of finishing the batch and exiting 1.
- Behavior change: the engine and
attest-engine doctorrefuseATTEST_EVAL_CONCURRENCY,ATTEST_EMBEDDING_CACHE_MAX_MB,ATTEST_JUDGE_CACHE_MAX_MB,ATTEST_HISTORY_MAX_ROWS,ATTEST_HISTORY_MAX_AGE_DAYSandATTEST_JUDGE_TIMEOUT_Svalues that are not positive integers (doctor exits 1 naming each variable).ATTEST_JUDGE_TIMEOUT_Sno longer falls back to 30 on a bad value. - Behavior change: JSON-RPC requests without an
idare no longer answered; parse errors and invalid ids reply withid: nullinstead of0. String ids are echoed unchanged. capture,replayandworldno longer die and orphan the wrapped command when SIGINT or SIGTERM arrives just as the command starts; the signal is relayed to the command.- The engine Go module path is
github.com/attest-framework/attest/engine, sogo install github.com/attest-framework/attest/engine/cmd/attest-engine@<version>resolves; the CONTRIBUTING clone URL points atattest-framework/attest. - Behavior change: the TypeScript SDK reads
ATTEST_ENGINE_TIMEOUTin seconds (was milliseconds) and refuses values above 3600 with “ATTEST_ENGINE_TIMEOUT is in seconds since 1.1.0”. - Behavior change: the repository’s TypeScript test run fails once with a
make engineremedy whenbin/attest-engineis missing or stale, instead of skipping or failing each engine-backed test.uv run pytestinsdks/pythonworks on a fresh clone via adevdependency group. - Behavior change:
threshold="dynamic"onsimilar_toandpasses_judgeis evaluated against history instead of always hard-failing as an invalid spec. - Behavior change: the dynamic pass bar is never below the static threshold, uses a sigma floor of 0.02, and is built from passing and soft scores only. An all-zero or failing history no longer passes a 0.0 score, and a steady 1.0 history no longer hard-fails 0.99.
- Behavior change:
query_driftandresult.driftuse the same estimator as classification;query_driftignoreswindow_size, andresult.driftis attached only when a 10-run baseline exists. - Behavior change: history is keyed by id scheme 2 assertion ids. Rows recorded under ordinal ids are orphaned, so the first runs after upgrading rebuild each baseline.
ContinuousEvalRunner(Python and TypeScript) delivers drift alerts: after each evaluation it reads drift and posts to the alert webhook / Slack URL ondrift_detectedorhard_fail. Dispatch errors go toon_error(Python) oralertError/onError(TypeScript) and the log, Python POSTs are time-bounded, andstop()waits for in-flight evaluations and dispatches.- Python
ContinuousEvalRunnerreads the alert webhook and Slack URL configured throughattest.config()(or theATTEST_ALERT_*env vars) when none are passed; TypeScriptstop()processes the traces already queued before resolving, bounded bydrainTimeoutMs. - Behavior change: strict replay keeps the roles of email identities across a session. A request that notifies a different address than the one looked up no longer replays a recording where both were the same address. A run that uses one different address throughout still replays.
- Behavior change: the test
attest capturedrafts for a command capture re-runs the agent under strict replay and fails when the new run misses the recording or violates a contract; it used to pass whatever the agent did. - Behavior change: an OpenAI refusal now fails
output_not_containson the refusal text; before, the output was empty and the assertion passed. - Behavior change: TypeScript adapter traces now match the Python adapters:
OpenAIAdaptertool-call args are objects (was the raw JSON string, which failedtoolArgsMatchSchema), refusals are the output text (sooutputNotContainson refusal wording now fails), and llm steps carry token counts. Tests that asserted the old shapes must be updated. - Behavior change:
LangChainAdaptermethods now take langchainjs’s callback signatures (..., runId, parentRunId). The old hand-called forms (handleChainStart(),handleLLMEnd(output),handleToolEnd(output, {name})) are gone: langchainjs never called them, becausefromMethodscopied only own properties.new LangChainAdapter()now throws unless the@langchain/corepeer is installed. - The
LangChainAdapterchange applies to the TypeScript SDK. It is aBaseCallbackHandlerkeyed byrunId; pass it incallbacks. - Adapters: OpenAIAdapter handles Responses API objects (Python and TypeScript) and records a model refusal as the completion and trace output; AnthropicAdapter records
server_tool_useblocks as tool calls; GeminiAdapter skips thought-summary parts, reads the model frommodel_version/modelVersion, reads usage tokens and no longer raises on a function-call-only response (TypeScriptinputis{text}for a single string message); OllamaAdapter (TypeScript) no longer records an emptymodelarg. - Provider adapters set
latency_msfrom the timestamps passed in (startedAtMs/endedAtMsin TypeScript) and record input, output and total tokens on the llm_call step, with tool calls after it. LangChainCallbackHandler (Python) records tool arguments as structured objects, reads ToolMessage content blocks andusage_metadatatokens, and measures latency from event timestamps.
Security
- Behavior change: the SDK engine download is verified against a hash table embedded in the wheel and npm tarballs instead of the release’s
checksums-sha256.txt. A binary that differs fails withdoes not match the hash shipped in attest-ai <version>/@attest-ai/core <version>. - Behavior change: a source or editable install no longer downloads an engine; it fails naming
ATTEST_ENGINE_PATHandmake engine. - Engine downloads refuse a redirect to a non-https URL, resolve relative redirects, and time out (10s connect, 120s total). A truncated download is reported as truncated rather than as a hash mismatch, and a malformed redirect
Locationis a normal download failure in both SDKs instead of an uncaught exception. - Release engine binaries carry GitHub build-provenance attestations, created by a dedicated
attest-enginejob that alone holdsid-token: writeandattestations: write; verify withgh attestation verify <file> --repo attest-framework/attest --signer-workflow attest-framework/attest/.github/workflows/release.yml --source-ref refs/tags/v<VERSION>. - The Python engine download honours
HTTPS_PROXY/NO_PROXYand system proxies (CONNECT tunnel, end-to-end TLS verification); proxy credentials never appear in errors.
Removed
@attest-ai/vitestno longer shipsengine-checksums.json(it has no downloader).- Empty
engine/pkg/engine,engine/internal/configandsdks/godirectories, unused helpers, and theengine-benchmarkandprotocol-benchmarkMakefile targets.
1.0.1 — 2026-11-05
Hotfix themed “Red stays red”. On 1.0.0 a failing assertion could be reported as green: an engine drift line, a request timeout or concurrent evaluations could hand one test another test’s verdict, a pytest-xdist run wrote an empty report that the policy gate passed, and a regression policy without its baseline passed silently. 1.0.1 routes every engine response by request id in both SDKs, writes complete reports under parallel runners, and makes attest policy evaluate fail closed when an input is missing. Several fixes turn a silent pass into a failure; they are marked Behavior change below.
Fixed
- Behavior change: Python and TypeScript SDKs route every engine response by request id. A test that previously passed on another request’s response (after an engine
drift_alertline, after anATTEST_ENGINE_TIMEOUT, or underit.concurrent) now reports its own, possibly failing, verdict. Lines without an id are dropped and logged. - Behavior change:
evaluate_batchraisesProtocolDesyncError(Python) or rejects with it (TypeScript) when the response’s assertion ids differ from the ids sent. - Behavior change: a request timeout restarts the engine; the timed-out request raises (rejects with)
EngineTimeoutErrorand every other pending request fails, so a late reply never reaches another test. The new engine gets the remainingATTEST_MAX_JUDGE_CALLS/ATTEST_BUDGET_USDsession budget: the configured limit minus the usage already returned, minus a hold for each lost request that can call a model (its per-batchbudgetif it sent one, otherwise everything left). Spend the SDK cannot observe (simulated-user messages, engine error replies, replies dropped after cancellation) leaves the restarted engine no budget, so a restart can never refill the session budget. - Behavior change:
pytest -n N --attest-report=<path>writes one report containing every worker’s tests and assertions; before, the report was empty andattest policy evaluatepassed. If a worker ends without sending its report, the run exits with an internal-error status and no report is written (an older file at the path is removed). - Behavior change:
attest policy evaluateexits 3 instead of passing when the policy setsblock_on_regressionorregressionwithout--baseline, ormin_mutation_scorewithout--mutation; the message names the missing input. - Behavior change:
attest policy evaluateexits 3 when a report has no tests while itstests_collectedis greater than 0 (a parallel run that lost its results). - The engine reports drift inside
evaluate_batchresults (results[].drift) instead of writing an id-lessdrift_alertline to stdout. - Python: responses over 64 KiB (for example 400+ assertions) no longer crash the session; the SDK’s stream limit matches the engine’s 10 MiB request line limit.
- TypeScript:
EngineManager.start()rejects when the engine exits or closes before it is ready, with the engine’s stderr tail (also when the engine dies before the first write, which used to surface as a barewrite EPIPE), andinitializetimes out instead of hanging. Write errors after startup carry the stderr tail. - TypeScript: request timers are cleared, so a standalone script exits when its work is done instead of 30 s later.
- Python:
EngineManager.send_requeststays public and id-correct (it goes through an attachedAttestClient, otherwise the id-matched handshake), andEngineManager.stop()shuts down cleanly when anAttestClientreader is attached. A secondAttestClienton one engine raisesRuntimeErrorinstead of stealing the first one’s responses. - Engine history and caches share one SQLite handle with WAL and
busy_timeout, so parallel engine processes no longer lose history rows toSQLITE_BUSY;attest baseline pinandattest calibrateopen the database the same way and wait instead of failing. - History pruning runs in the background in short batched deletes (age through a new
created_atindex, the row cap per recently recorded assertion id) instead of a whole-table delete insideevaluate_batch, removing the stall near 2,800 evaluations per assertion. - The
attest-aiPyPI quick start is a runnable pytest test that evaluates its chains and fails on a wrong assertion; it used an undefinedtraceand built chains it never evaluated. - Guide examples in framework-adapters, multi-agent and simulation evaluate their chains and use existing methods (
required_tools()replaces the nonexistenttool_called()).
Added
- report-v2 files written by the pytest plugin and the Vitest reporter carry a top-level
tests_collected(tests collected after deselection);report-v2.schema.jsondeclares it.
Changed
- The protocol spec states that the engine writes only responses to stdout and that SDKs route by request id;
docs/guides/configuration.mddocumentsATTEST_ENGINE_TIMEOUT(seconds in Python, milliseconds in TypeScript) and what a timeout does to the session budget, and notes that budgets apply per pytest-xdist worker. - Install instructions use
attest-ai>=1.0.1and@attest-ai/core@>=1.0.1/@attest-ai/vitest@>=1.0.1;@attest-ai/vitestdeclares the peer@attest-ai/core >=1.0.1. The annotations guide pinspr-annotate@v1.0.1. - The PyPI classifier is
Development Status :: 4 - Beta(was5 - Production/Stable) until 1.1.0 completes its re-evaluation. - Engine Go dependencies:
go.opentelemetry.io/proto/otlp1.11.1,golang.org/x/text0.43.0,modernc.org/sqlite1.61.1,google.golang.org/protobuf1.36.12.
Removed
- The
drift_alertstdout notification (types.DriftAlertNotification).
1.0.0 — 2026-11-04
First stable release. A real run or incident becomes a verified fixture with attest capture; pull-request CI replays the agent’s real code against it with zero model calls and returns pass, fail or not evaluated (with a reason) to a policy gate, in Python and TypeScript. The release candidates added each piece: honest verdicts and install integrity (rc.1), trace checks, behavior contracts and judge budgets (rc.2), capture, replay, world fixtures and pull-request annotations (rc.3), agent-facing CLI, MCP server and mutation testing (rc.4), TypeScript parity, framework capture through OpenTelemetry and the offline failure viewer (rc.5). The refund-desk demo walks the whole workflow with make walkthrough. Changes since rc.5:
Changed
- Install instructions in the README, the Python SDK README (the PyPI page), the guides and the
attestskill’s migration reference useattest-ai>=1.0.0and@attest-ai/core@>=1.0.0/@attest-ai/vitest@>=1.0.0(withvitest@^4) instead of the release-candidate floors; with 1.0.0 published, a bare install resolves to it. The annotations guide’s example pinspr-annotate@v1.0.0.@attest-ai/vitestdeclares the peer@attest-ai/core >=1.0.0(was>=1.0.0-rc), so it no longer installs next to a release-candidate core with a different engine version. The PyPI classifier isDevelopment Status :: 5 - Production/Stable.
Fixed
attest capturenever reports a false green. A capture exits 1 (not verified, naming every cause that holds) when a drafted contract failed, the trace check failed,--expect-faildrafted nothing, or nothing was drafted while the command exited non-zero or a replay-capable capture recorded no model call. Before, a command that exited non-zero with no reachable model printedverified: every drafted contract passeswith zero drafted contracts and exited 0. A non-zero exit whose drafted contracts all pass still verifies (exit 0, the code recorded).attest trace checkwarns on a not-verified fixture with the same causes.- A healthy run with nothing to draft a contract from (no tool call, structured output or cost) has the new outcome
nothing_to_verify: the fixture is written withverified: false,attest captureexits 0 and printscaptured: nothing to verify (no tool call, structured output or cost); replay-only fixture, no test drafted, and no test is drafted.attest capture --jsonand--result-filegainoutcome(verified|not_verified|nothing_to_verify;proto/attest/v1/cli/capture.schema.json, with valid and invalid fixtures). Pythonrecord()and TypeScriptrecord()returnverified=Falsewithoutcome="nothing_to_verify"instead of raising, and still raiseCaptureErrorfornot_verified. The committed judge fixtures were re-recorded and no longer claimverified: truewith no contracts. - Re-capturing the same scripted run writes the same
trace.conversation_id: clock-derived conversation ids (trace.conversation_id, thegen_ai.conversation.idspan attribute) are templated to{{conversation_N}}. The capture guide now names what still differs between captures instead of promising identical fixtures: raw span ids inturns[].from_agent(OpenAI Agents), LangChain message andlc_run--ids in LangSmith strings, and the Claude Code session id inmetadata.user_idof the recorded request (Claude Agent SDK), which is part of the replay key’s params.
1.0.0-rc.5 — 2026-11-04
Fifth release candidate, themed “Parity and reach”: the TypeScript SDK gains the simulation helpers and plugin discovery Python has, with a generated parity matrix that fails the test suites on an undocumented gap; attest capture records runs from Pydantic AI, the Vercel AI SDK, Mastra, the OpenAI Agents SDK (Python), the Claude Agent SDK and LangGraph (Python) through their OpenTelemetry instrumentation; and attest view renders a self-contained offline failure page with the failing span highlighted and a baseline diff.
Added
attest view <report.json...> [--baseline <report.json>...] [--mutation <mutate.json>] [--out FILE] [--no-open] [--json]writes one self-contained offline HTML page: the test list, the trace tree with the failing span highlighted, expected versus actual, replay or live provenance, cost, a baseline-versus-head diff (pass rate, cost, latency, changed tool sequences) and surviving mutants. The page has no JavaScript and no external resources, carries adefault-src 'none'Content-Security-Policy, escapes all report content and bounds every field and the page size. It opens in a browser only on a terminal outside CI.--jsonschemacli/view.schema.json; exit 0 written, 2 usage or input error. Guide:docs/guides/viewer.md.ATTEST_REPORT_TRACES=1makes the pytest plugin and the Vitest reporter add a top-leveltracesmap (trace id to trace-v2) and atrace_idon each result row to report-v2. Both fields are optional inreport-v2.schema.json;report_versionis unchanged. Traces hold prompts, outputs and tool arguments, so the variable is off by default.attest init --ci github(Python and TypeScript) adds aWrite the failure viewerstep and uploadsattest-report.htmlin theattest-reportartifact. The test step carriesATTEST_REPORT_TRACES: "1"commented out with a warning: traces are not redacted and artifacts of public repositories are downloadable by any signed-in GitHub user, so enable it only on private repositories.- Framework capture through OpenTelemetry, with no attest-specific adapter:
docs/guides/capture.mdgains a Frameworks section with the instrumentation switch, tested versions and caveats for Pydantic AI, the Vercel AI SDK, Mastra, the OpenAI Agents SDK (Python), the Claude Agent SDK (Python and TypeScript) and LangGraph (Python). Golden OTLP fixtures for each route joinprotocol-tests/trace-v2/otlp/. - The OTLP parser keeps
tool.outputspan events (the first four per span, among the first 256 events); all other events are never read. A malformedtool.outputevent is dropped andtrace checkwarnsmalformed_event. Claude Code records a tool’s result there. - Claude Code native traces (beta):
claude_code.llm_requestis a model call with its native model, token and cache-token attributes;claude_code.tooltakes its result from itstool.outputevent;claude_code.tool.executionandclaude_code.tool.blocked_on_userare not steps. A native attribute of the wrong type is skipped with amalformed_native_attributewarning fromtrace checkinstead of failing conversion. - Tool spans without
gen_ai.tool.call.argumentsorgen_ai.tool.call.resultfall back togen_ai.promptandgen_ai.completion, decoding LangSmith’s result wrapper shapes on LangSmith spans. - TypeScript simulation helpers in
@attest-ai/core:scenario,faultInject,mockTool,MockToolRegistryandrepeat, withScenarioResultandRepeatResultaggregating verdicts like Python’s.TraceBuilder.addToolCallreturns the active mock’s result. Guide: simulation. - TypeScript
useAttest().enforce(verdict)andenforceOutcomeaccept aScenarioResultorRepeatResultas well as anAgentResult(fail with diagnostics, skip with the not-evaluated reasons, strict mode fails skips), as Python’sattest.enforcedoes. - TypeScript plugin discovery:
loadPackagePlugins(registry, projectRoot?)loads plugins that an installed dependency declares inpackage.jsonunder"attest": {"plugins": [...]};registerPluginmirrors Python’sregister_plugin. - A parity matrix, docs/guides/parity.md, generated from both SDKs’ public surfaces (
sdks/parity/). The existing Python and TypeScript test commands fail on a Python capability with no TypeScript counterpart or documented exception, on a stale exception, and when the snapshots or the guide are out of date (regenerate withATTEST_UPDATE_PARITY=1).
Changed
- A
tool_cardinalitycheck that fails onmaxnow setstrace_node_pathtosteps[N], the first call over the limit, instead oftrace.tool_cardinality; aminviolation keeps the old path. Report-v2 rows and the SDK diagnostics output (trace path:) show the new value;attest report rowsoutput does not change. - A span whose
gen_ai.operation.nameis present but unknown (chain,agent_step,unknown, …) is no longer a step; its children still attach to the nearest enclosing step. Spans with no operation attribute keep the attribute heuristics. attest capturedoes not verify a fixture where nollm_callortool_callstep was mapped (exit 1, with the cause and a pointer to the capture guide). Thetrace checkno_genai_spansmessage points to the same guide section.
Fixed
- Finish reasons
tool-calls(Vercel AI SDK, Mastra) and a JSON array in a string, as Mastra records it, count as tool requests intrace check; it no longer warnsunrequested_toolon those exports. - TypeScript
executePluginAssertionclears its timeout timer when the plugin finishes, so a completed assertion no longer holds the process open for the timeout.
1.0.0-rc.4 — 2026-11-04
Fourth release candidate, themed “Agent-native”: coding agents operate attest without the website through --json output with published schemas and stable exit codes, offline guides (attest guide), installable skills and plugins, and an MCP server whose closed contract grammar drafts and checks contracts. attest mutate measures how much of an agent’s behavior the suite would actually catch.
Added
attest mutate <fixture.attest.json...> [--contracts DIR] [--seed N] [--max-mutants N] [--json]tests the tests: it builds behavior mutants of each fixture (drop, duplicate or reorder tool calls, perturb arguments, inject tool errors, alter world effects, truncate output), evaluates the suite’s deterministic contracts on every mutant at no model cost, and reports a mutation score, each surviving mutant and a verified contract that would kill it. The mutant set is identical across runs for a fixed seed.--jsonvalidates againstcli/mutate.schema.json; exit 0 report produced, 1 no contract applies to any fixture, 2 usage or load error. Seedocs/guides/mutation.md.ATTEST_CONTRACTS_DIRmakes each engine process record the assertion specs it evaluates (one0600NDJSON file per process, no traces or outputs) soattest mutate --contractscan replay a suite’s contracts.evaluate_batchtakes an optionalcontext: {test_id, file, line}that the pytest plugin and the Vitest integration send for the running test; the engine records it next to each spec and does not otherwise use it.- The policy rule
min_mutation_score: {score, severity}withattest policy evaluate --mutation <mutate.json>: a score below the threshold exits 1 (block) or 2 (warn). A policy with the rule and no--mutationis a load error (exit 3). attest guide [<topic>]prints the shipped guides (docs/guides/*.md) from the engine binary, offline;--jsongives{topics:[{name,title}]}or{topic,title,markdown}. New guides:cli(agent-facing CLI reference, exit-code table) andagents(skills,init --skills, plugins). Guides are staged into the engine module bygo generate ./internal/assets/(make generate) and embedded; a test ingo test ./...fails when the staged copies drift.- Every engine command and subcommand accepts
--jsonand then writes exactly one JSON document to stdout on success; on a usage, load or runtime error it writes nothing to stdout and the exit code plus stderr describe the error. Schemas are inproto/attest/v1/cli/(doctorkeepsproto/attest/v1/doctor.schema.json) with valid and invalid fixtures inprotocol-tests/cli/, tested in Go, Python and TypeScript.capture,replayandworldwith-- <command>send the command’s stdout to stderr under--jsonand print one result object that carries itsexit_code.attest help --jsonlists every command with its schema and exit codes.attest init --json(both SDKs) prints{created, updated, skipped}, and the SDKs addinittoattest help --json. - Each command declares its exit codes in the registry; the table in
docs/guides/cli.mdis checked against it by a test. attest skills list|install [--dir D] [--json]writes the embeddedattestandattest-verify-generatorskills to.agents/skills/(Codex, Cursor) and.claude/skills/(Claude Code), touching only attest-owned files. It validates every destination before writing, follows directory symlinks that resolve inside--dir(writing each real file once) and refuses symlinks that resolve outside it or symlinked skill files, writing nothing.attest init --skills(Python and TypeScript, alone or with--ci github) calls it. Theattestskill routes to writing contracts, capturing an incident, debugging a failure, CI setup and migrating from Promptfoo or DeepEval with aMIGRATION.mdprogress file;attest-verify-generatorinterviews a repository and writesverify-<agent>skills.attest mcp [--root D]serves attest to coding agents over MCP on stdio with a fixed toolset:trace_check,evaluate,explain_failure,draft_contract,run_contractandfixture_info. It is read-only and offline (deterministic layers only; judge and embedding assertions returnnot_evaluatedwithprovider_not_configured) and confined to the project root: paths are resolved through symlinks, files are capped at 32 MiB.run_contractparses statements such astool_cardinality("issue_refund", per="order_id", at_most=1)with a closed grammar, never as code, and returns per-statement results plus the Python and TypeScript test lines.attest mcp --tools --jsonprints the tool list;attest mcp install --dir D --command <json argv>andattest init --mcp(Python and TypeScript) write.mcp.json,.cursor/mcp.jsonand.codex/config.tomlwithout touching other servers. Guide:mcp.- Claude Code, Cursor and Codex plugin manifests (
.claude-plugin/,.cursor-plugin/,.codex-plugin/) publish the sameskills/; they are generated fromVERSIONand one metadata source bygo generateand checked for drift. In Claude Code:/plugin marketplace add attest-framework/attest.
Security
- JSON Schema assertions (
type: schema) resolve$refinside the schema document only:file://andhttp://references no longer load, for every engine caller. Previously a schema could read any local file the engine could, and its compile errors could echo part of it. The compiled-schema cache is bounded.
Changed
attest policy evaluateexits 3 on usage errors and when the policy, report or baseline fails to load (previously 2 and 1), so they cannot be read as a warning or a block. Verdicts keep 0 pass, 1 block, 2 warn.attest cache,baseline,versionandpolicy evaluatereject unexpected arguments;cacheusage errors exit 2 (previously 1).attest capture --refreshalso refuses to prompt (exit 2,--yeshint) whenCIis set or--jsonis given.
1.0.0-rc.3 — 2026-11-03
Third release candidate, themed “Flight recorder”: capture a real run or incident as a verified fixture, replay the agent’s real code against it in CI with zero model calls, check what the agent did to a simulated world with effect contracts, and get a pull-request summary, inline annotations, JUnit and diff-based test selection.
Added
attest capture --name <name> -- <command>turns a run into a regression fixture. The engine starts a loopback listener hosting a recording proxy for OpenAI Chat Completions, OpenAI Responses and Anthropic Messages (streaming and not) and an OTLP/HTTP trace receiver (http/protobufandhttp/json, optionally gzip), runs the command withOPENAI_BASE_URL,ANTHROPIC_BASE_URLandOTEL_EXPORTER_OTLP_TRACES_ENDPOINTpointing at it, forwards model calls to the upstreams set by those variables in the capture’s own environment (defaulthttps://api.openai.com/v1andhttps://api.anthropic.com), and writestests/attest/fixtures/<name>.attest.json(--out-dirto change). Request headers are never recorded; values of environment variables whose names end inKEY,TOKEN,SECRETorPASSWORDand key-shaped strings (sk-…,AKIA…,ghp_…, bearer credentials, private keys) are replaced by[redacted]in keys and values. Trace and span ids, provider response and tool-call ids, timestamps, durations and emails are templated to stable placeholders across the trace, the exchanges and their replay keys, so capturing the same scripted run twice writes the same bytes (timestamps, numeric or ISO-8601, are numbered by position, never by value: the fixture is walked with each object’s start-like keys first and end-like keys last, so every start precedes its end and a parent brackets its children, and runs that differ only in timing write the same bytes). A model call the agent cancels or does not read to the end, and an OTLP export request still in flight at the end or rejected, fail the capture, since the fixture could not replay the run. SIGINT and SIGTERM sent toattest captureare forwarded to the command.--from <otlp.json>builds a trace-only fixture (not replay-capable) from an export;--servehosts the listeners for in-process helpers.attest capture --check-redaction <fixture>exits 1, naming the JSON path, when a fixture holds a secret.- Capture drafts deterministic contracts from the run as ceilings on what it observed (the tools called, the order of their first calls, per-id cardinality for each tool called with an
id/*_idargument, the structured output’s shape, and a cost ceiling at 1.5× the observed cost) and marks the fixture verified only when they all pass and the captured trace passes trace check; otherwise it exits 1 naming the reason. A normal capture drafts each tool’s cardinality at the observed per-id maximum, so a healthy run that reads an id twice verifies.--expect-failsaves an incident reproduction: cardinality is drafted at one call per id, the fixture is verified when drafted contracts fail, and those are recorded as expected failures (INC-1042:cardinality-issue_refund-per-order_id). - Python capture helpers:
with attest.record("<name>", expect_fail=...)captures the code in the block by startingattest-engine capture --serveand settingOPENAI_BASE_URL,ANTHROPIC_BASE_URLandOTEL_EXPORTER_OTLP_TRACES_ENDPOINTfor the block (construct model clients and the OpenTelemetry exporter inside it; the global tracer provider is flushed on exit), and raisesattest.CaptureErrorwhen the fixture is not verified. Captures cannot overlap (record()sets process-wide variables).attest.load_fixture(path)reads a fixture. For a verified fixture,attest captureandrecord()drafttests/attest/test_<name>.py(name lowercased, other characters as_) from its contracts with theexpect()DSL, unless that file exists: a verified fixture’s test enforces them withattest.evaluate, an incident reproduction’s test checks withattest.inspectthat the expected failures still fail. - TypeScript capture helpers:
record(name, fn, { expectFail, flush })from@attest-ai/corecaptures whatfndoes throughattest-engine capture --serve(pass the tracer provider’sforceFlushasflush) and throwsCaptureErrorwhen the fixture is not verified;loadFixture(path)reads a fixture. For a verified fixture,npx attest captureandrecord()drafttests/attest/<name>.test.tswithattestExpectandinspect/evaluatefrom@attest-ai/vitest, unless that file exists. Captures cannot overlap. Every drafted contract has a TypeScript DSL call. Seedocs/guides/capture.md. - Fixture schema v1 (
proto/attest/v1/fixture-v1.schema.json, fixtures inprotocol-tests/fixture-v1/): the trace-v2 trace, the exchanges with a replay key (model, parameters, prompt hash, tool-schema hash), the drafted contracts, their verification and aworldsection for the simulated world’s final state (nulluntil--worldfills it).attest trace checkaccepts a fixture. attest-engine replay --name <name> -- <command>serves a fixture’s recorded model responses to the real agent: the engine’s proxy answers every OpenAI Chat Completions, OpenAI Responses and Anthropic Messages request from the fixture (streamed exchanges replay as events) and makes no provider call. Requests are matched on model, parameters, prompt and tool schemas after the capture redactor is applied to the live request and every volatile value (ids, timestamps, emails, and the{{id_N}}placeholders a replayed response hands back to the agent) is renumbered by first occurrence within that request, so a live request needs no placeholder numbering of its own; identical retries consume successive recordings in order.--mode strict(default) answers a request with no recording with an error naming the first component that changed, for examplereplay miss: system prompt changed (hash a1b2c3d→e4f5a6b); re-record with \attest capture —refresh inc-1042`;—mode autoforwards it live only when the request carries exactly theOPENAI_API_KEYorANTHROPIC_API_KEYof the engine's environment for that provider (the proxy adds no credentials), and otherwise reports the miss;—no-liverefuses every live forward. A model name in a miss message is quoted.replay —serveis the long-lived form for SDK helpers (environment line,stop`, summary line with replayed, live and missed requests and the cost of live calls the built-in price table covers).attest capture --refresh <name>re-runs the fixture’s recorded command with its original flags (--expect-failincluded) and overwrites the fixture. The command comes from the fixture file, so it is printed shell-quoted first and runs only after--yesor ayat a terminal (otherwise exit 2, nothing runs); it refuses when the command’s arguments were redacted or templated, or the fixture came from--fromor an in-process capture.- Report-v2 gains optional replay provenance:
tests[].replay(fixtures,mode,replayed,live,misses,miss) andsummary.replay(replayed,live,misses,live_cost_usd,unpriced_live_calls).summary.usagekeeps counting only attest-initiated model calls. Valid and invalid examples are inprotocol-tests/report-v2/. replay_missjoins thenot_evaluatedreasons (ReasonReplayMissin Go,proto/attest/v1/report-v2.schema.json,protocol-tests/verdicts/verdict.schema.json): a fixture-bound test inautoreplay mode without a provider key reports it. The SDKs set it; the engine’s own batches never return it.- Python replay: the
attest_replay("<name>")pytest fixture (andattest.start_replay()) runs the agent’s real code against a fixture’s recorded model responses.--attest-replay=strict|auto|off(alsoATTEST_REPLAY; defaultstrictwhenCIis set,autootherwise) chooses what a request with no recording does:strictfails the test with the engine’s miss message, naming the changed component and theattest capture --refreshcommand, whatever the agent raised and whatever marker the test carries (xfail(strict=True)and Vitestit.failscannot turn it into an expected failure);autogoes live when a provider key is set and otherwise skips the test asnot_evaluated: replay_miss(a failure under--attest-strict). The terminal summary printsattest replay: N replayed · M live model calls · $X.XX, and--attest-reportwrites the per-test and summary replay provenance. Seedocs/guides/replay.md. - TypeScript replay:
@attest-ai/coreaddsstartReplay()and@attest-ai/vitestaddsuseReplay(name, { mode, egress, strict }), with the same modes (ATTEST_REPLAY=strict|auto|off) and outcomes.AttestReporterwrites the replay provenance and prints the same summary line. - Provider-egress guard:
pytest --attest-egress=block,ATTEST_EGRESS=blockoruseReplay(name, { egress: "block" })fails in-process connections toapi.openai.com,api.anthropic.com,generativelanguage.googleapis.comand*.openai.azure.com; loopback is always allowed. It is off by default. With the guard on, replay runs the engine with--no-live, so no miss goes live. Control and format characters in miss and engine-error text are escaped before they reach a test report. - Attest’s own L6 judge integration tests (
sdks/python/tests/integration/test_judge_replay.py) runpasses_judgethroughATTEST_JUDGE_BASE_URLpointed at the replay proxy, from fixtures recorded against a scripted OpenAI-compatible judge.make record-judge-fixturesre-records them against a real provider; it needsATTEST_JUDGE_API_KEY(orOPENAI_API_KEY) andATTEST_JUDGE_MODELand refuses to run without them. attest-engine world --file world.yaml -- <command>serves the external system an agent acts on as a local stateful HTTP fake.world.yaml(schemaproto/attest/v1/world-v1.schema.json, fixtures inprotocol-tests/world-v1/) declares JSON resources with create, list and get routes, an idempotency-key header for creates, seed records and a fault script, and names the environment variable that carries the world’s base URL (for examplePAYMENTS_BASE_URL). A repeated idempotency key replays the first response and commits nothing. Faults:timeout_after_commitcommits the create, then holds the response until the client disconnects (30 s cap), so the outcome never depends on a wall-clock race;429and500fire on listed request ordinals or by probability from a seeded generator, and the same seed and request sequence give the same outcomes. A malformed file exits 2 naming the JSON path (/resources/refunds/routes/1).--serveprints{"env":...}, serves untilstop, then prints{"world":<final snapshot>};--state-outwrites the snapshot.GET <base URL>/_attest/statereturns the current state as a world-v1 snapshot.attest capture --world world.yamlandattest-engine replay --world world.yamlhost the world in the same session as the recording or replaying proxy. Capture stores the world’s final state (world_version, definition hash, state, faults fired and their count) in the fixture’sworldsection, redacted like the rest of the fixture, and email addresses in record keys and values are templated to{{email_N}}with the same numbering as the trace (one address, one token across the fixture; ids and timestamps in records are kept as the world held them). The section is evidence only:replay --worldstarts a fresh world fromworld.yaml, not from the fixture. A capture without--worldkeepsnull.attest capture --refreshrefuses a fixture that carries a world section, since a refresh would drop it.- An over-long request line (over 10 MiB) no longer kills the engine: it is answered with a
-32600REQUEST_TOO_LARGEerror carrying the request’s id (null when the id is not in the first bytes) and the engine keeps serving. effectassertions (Layer 3, deterministic, no new layer) check what the agent did to the world:countof matching records (equals,at_most,at_least),uniqueby fields, andidempotentkeys. The spec embeds a world-v1 snapshot as its evidence, so the evaluator interface is unchanged. A violation reads, for example,effect violated: 2 refunds for O-1042 (expected 1). Anullsnapshot isnot_evaluatedwith reasonmissing_evidence. Seeproto/attest/v1/protocol-spec.md.- Python world fixtures:
attest.start_world(path)(and theattest_world(path)pytest fixture) hosts aworld.yamlthroughattest-engine world --serveand sets its base URL variable (PAYMENTS_BASE_URL) until the test ends; the session hasenv,base_url,state()(the current world-v1 snapshot, fromGET <base URL>/_attest/state) andstop()(the final snapshot). It composes withattest_replayin one test (two engine processes, merged environment).attest.record(..., world=path)captures with--world.expect(trace).world(snapshot)addscount(resource, equals=, at_most=, at_least=, **where),unique(resource, by=, **where)andidempotent(resource, key=, **where)effect checks;world(None)isnot_evaluated(missing_evidence). - TypeScript world fixtures:
startWorld(path)from@attest-ai/coreanduseWorld(path)from@attest-ai/vitest(composes withuseReplay) with the same session,record(name, fn, { world }), andattestExpect(trace).world(snapshot)withcount(resource, { equals, atMost, atLeast, where }),unique(resource, { by, where })andidempotent(resource, { key, where }). Both SDKs emit the same specs;protocol-tests/contracts/effect-dsl.jsonpins them and the engine’s result for each. docs/guides/world-fixtures.md: the world model, theworld.yamlreference with the INC-1042 payments example, faults and determinism, idempotency semantics, running a world, the effect DSL, composing with replay, and limits.- Effect checks guard the request size:
expect(...).world(snapshot)measures the snapshot once and raisesValueError/Errorbefore sending when the checks of one chain would embed it more than 9 MiB in total (MAX_WORLD_BYTES), since the engine limits a request line to 10 MiB. The Python codec sends UTF-8 instead of\uXXXXescapes, so non-ASCII text no longer inflates up to 6x. In Pythonresourceis positional-only and filters can be given as awheredict (use it for fields namedkey,byorequals); a filter given both ways raisesTypeError. TypeScriptlayerForType("effect")returns 3, like the engine. attest report summary --report <path>... [--policy-result <file>]prints the pull-request summary as Markdown:### attest: N passed · N failed · N not evaluated(plus· N skippedfor plain skips and expected failures),N replayed · N live model calls · $X, the usage line with retry cost, not-evaluated counts by reason, a capped table of failures and replay misses withfile:line, and the policy verdict. Several reports merge; unreadable input exits 2.attest report junit --report <path>... --out <file>writes JUnit XML from report-v2 (one suite per test file,fileandlineon each case,<failure>for failed tests,<skipped>for skipped tests, with the reasons when their assertions were not evaluated).attest report rows --report <path>... --jsonprints the failures and replay misses as{file, line, test_id, assertion_id, type, status, reason, explanation}rows. A strict-mode replay miss, which fails its test without an assertion row, appears as areplay_missrow.attest select --base <ref> [--json] [--repo <dir>]lists the tests a diff needs: touched test files, and the tests that reference a touched fixture by capture name. Lockfiles, dependency manifests, attest config, world files and any file it does not recognize selectall; markdown selects nothing. Exit 2 when git or the ref fails.- Guides
docs/guides/baselines.md,policies.mdandselection.md. pr-annotatev2 (.github/actions/pr-annotate, ships in rc.3):reportaccepts several paths (newline or space separated) and the policy runs on each, the worst exit winning (an evaluator error, then 1, 2, 0). It posts one summary comment per pull request, rendered byattest report summaryplus a link to the run, found by the hidden marker<!-- attest:pr-annotate -->and updated in place; only marker comments written by the token’s own login are updated (the Actions bot, or the user for a personal token), so a planted marker is ignored. The summary is always written to the job summary too. The comment is posted only on same-repositorypull_requestevents; fork pull requests and tokens that cannot write get the job summary and a notice or warning, never a failed step. Failures and replay misses fromattest report rowsbecome::error(hard failures, replay misses) and::warning(soft failures, not-evaluated) annotations at the test’s file and line, relative to the repository root (a row’sfileis prefixed with its report’s directory), escaped per the workflow-command rules, with control characters stripped, messages capped, and at most 10 errors and 9 warnings per step (one annotation counts the rest). The engine never receives the token, the policy result is written underRUNNER_TEMPand not echoed, and##[is broken up in every logged string. The engine is found asengine-binary, thenATTEST_ENGINE_PATH, thenPATH, then~/.attest/bin/<engine-version>/attest-engine(engine-versiondefaults to the action’s ownVERSION). The optionalcomment-authorinput names the login whose comment is updated (<app-slug>[bot]for a GitHub App token, which cannot look itself up). Inputs and theexit-codeoutput are unchanged. Seedocs/guides/annotations.md.attest init --ci githubwrites anattest report junitstep and uploadsattest-junit.xmlwith the report, and setspermissionstocontents: readfor the workflow withpull-requests: writeon the job only (both SDKs).
Changed
- Behavior change:
pr-annotateno longer posts pull request review comments (they could only target lines in the diff). Failures are workflow annotations and the summary is one updated comment; the action now runsattest-engine report summaryandreport rows, so the engine is required even without apolicy. A caller pinned to@v1.0.0-rc.1or@v1.0.0-rc.2is unaffected until it moves to the rc.3 tag. attest report summaryrenders every report-derived string (test ids, file paths, explanations, not-evaluated reasons, policy rules and details) as an inline code span, with control, bidi and zero-width characters removed and the fence longer than any backtick run in the text, so agent output in a pull request comment cannot ping users, link issues, inject HTML or hide the rest of the comment.- The engine builds with Go 1.27 (
engine/go.modgo 1.27.0; CI and the release cross-compile use Go 1.27). Go 1.25 is outside Go’s two-release support window and no longer gets security fixes. Prebuilt engine binaries are unaffected; building from source, including through thesetup-attestaction (which reads the version fromengine/go.mod), needs Go 1.27 or later. - The Python and TypeScript SDK workflows build the engine (
make engine, Go 1.27) before their tests and run whenengine/**changes, so the engine-backed integration tests run in CI instead of skipping.
Security
- The capture listener binds
127.0.0.1on an ephemeral port and serves only under a random 128-bit path token handed to the command in its environment (other paths are 404 and nothing is recorded), refuses requests whoseHostis not a loopback name or that carry anOriginheader, admits 64 concurrent requests, bounds request and recorded response bodies at 32 MiB and a session’s recordings at 512 MiB (the capture fails past it), checks OTLP protobuf span, attribute, value and nesting limits on the wire before decoding it, and closes when the command exits. Provider credentials pass through to the upstream in memory only. Redaction covers secrets streamed across several response deltas (Chat Completions content, refusal, reasoning content, audio transcript and function or tool-call arguments; every Responses*.deltaevent; Anthropic text,partial_json, thinking and citations), which become[redacted]in the first delta. A response is a stream when its media type istext/event-stream(any parameters) or the request asked for one; a stream that does not parse fails the capture rather than being written. It also covers environment names ending in_PASS,_PWD,PASSWD,CREDENTIALS,CREDENTIAL,DSNorAUTH(notPWD,OLDPWDor*_ASKPASS), the password of any URL with userinfo, Stripe, Groq, Hugging Face and xAI keys, percent-decoded exchange paths withkey,api_key,apikey,token,access_token,sigandsignaturequery values, the values of secret-named command flags and ofNAME=valuearguments whose name marks a secret, and secrets an in-process block loads into its environment. Names from the trace are escaped in terminal output. - The replay listener is the capture listener in another mode:
127.0.0.1on an ephemeral port under a random 128-bit path token, loopbackHostonly, noOrigin, 64 concurrent requests. A fixture is read with a 256 MiB size limit and refused when it has more than 10,000 exchanges, a truncated or empty exchange, an unknown provider or a status outside 100–599. Live requests are redacted before matching, so a secret in a request never reaches a miss message or a summary, and replay answers a miss with HTTP 422, which the official SDKs do not retry. - The world listener is the capture listener:
127.0.0.1on an ephemeral port under a random 128-bit path token, loopbackHostonly, noOrigin, 64 concurrent requests.world.yamlis untrusted input: at most 256 KiB, one YAML document, string keys only, no unquoted timestamps, validated against world-v1 before it is used. The world refuses request bodies over 1 MiB, holds at most 10,000 records and 1 MiB of state, faults listed included (507beyond), lists at most 1,000 fired faults in a snapshot (faults_fired_countstays exact), rejects integers beyond 2^53-1 and non-finite numbers in request bodies and inworld.yaml, accepts only the query filters a resource declares, and releases held responses when the session closes.
Fixed
@attest-ai/vitestdeclaresvitest >=3.0.0 <5. Vitest 5 removedvitest/suite, which the plugin imports, so the old>=3.0.0range accepted a Vitest it cannot load. The install commands in the README,docs/guides/behavior-contracts.mdanddocs/guides/merge-gate.mdnow pinvitest@^4, since a barevitestinstalls Vitest 5. Vitest 5 support is planned.- TypeScript
OTelAdapter.fromOtlp/fromSpansno longer hang for 60 s and throwtrace convert timed outunder parallel load. They ran the engine withspawnSyncand the export on stdin, which on macOS occasionally never returned: about 1 call in 100 with 8 concurrent callers. The engine now runs as an asynchronous child process, which also stops the conversion from blocking the event loop.
1.0.0-rc.2 — 2026-11-02
Second release candidate, themed “Evidence”: check a trace’s integrity, write behavior contracts over what the agent did, get statistical verdicts and judge budgets, and judge with OpenAI, Anthropic, Gemini or a local ACP agent.
Added
- Native Anthropic (Messages API) and Gemini (
generateContent) judges:ATTEST_JUDGE_PROVIDER=anthropicorgemini, with keys fromATTEST_JUDGE_API_KEY,ANTHROPIC_API_KEY, orGOOGLE_API_KEYthenGEMINI_API_KEY,ATTEST_JUDGE_MODELrequired, andATTEST_JUDGE_BASE_URLfor gateways. Each provider has its own rate limit (ATTEST_RATE_LIMIT_ANTHROPIC[_BURST],ATTEST_RATE_LIMIT_GEMINI[_BURST]); an outage isnot_evaluated: provider_unavailable.doctorreports the new providers and key sources. Seedocs/guides/judges.md. - Judge results record the provider in
judge_metadata.providernext tomodel; reports printjudge: <provider>/<model>. attest calibrate --persiststores labels keyed by judge provider and model (--judge-provider,--judge-model), so calibration is never pooled across judges.ATTEST_JUDGE_PROVIDER=acpruns judges locally through an Agent Client Protocol agent started byATTEST_JUDGE_ACP_COMMAND(for example a Claude Code or Codex ACP adapter) on the developer’s own subscription. The engine refuses it whenCIor a CI vendor variable such asGITHUB_ACTIONSis defined. The agent runs in an empty temporary directory and its own process group, gets no file-system or terminal capability, and has every permission request rejected; it is not sandboxed, and the engine warns about this at startup. Every call, including simulated-user turns fromgenerate_user_message, is bounded byATTEST_JUDGE_TIMEOUT_S. The agent’s environment loses provider keys and endpoint switches (OPENAI_API_KEY,OPENAI_BASE_URL,CODEX_API_KEY,ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL,CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEX,GEMINI_API_KEY,GOOGLE_API_KEY,ATTEST_JUDGE_*), and logs name only the agent executable. Rate limit:ATTEST_RATE_LIMIT_ACP[_BURST]. Seedocs/guides/judges.md.attest trace check <otlp.json>reports the integrity of an OpenTelemetry trace export (orphan spans, parent cycles, duplicate span ids, unpaired tool calls and results, each with its span id) and its evidence coverage (model spans without tokens, cost, message content or model; tool spans without call id or content), as text or--json, exiting 1 on integrity errors.attest trace convert <otlp.json>turns the export into an Attest trace. Conversion from OTLP now runs in the engine on the current OpenTelemetry GenAI semantic conventions; malformed input is rejected with an error naming the span. Seedocs/guides/trace-check.md.- Model-call budgets enforced by reservation:
ATTEST_MAX_JUDGE_CALLScaps judge requests andATTEST_BUDGET_USDcaps the spend of judge and embedding calls per engine session;evaluate_batchaccepts a per-batchbudget(max_judge_calls,max_cost_usd). Every judge call,repeatrun, bias probe, simulated-user message, retry and paid embedding call reserves one call and its worst-case cost (price × prompt bytes + price × output-token limit) before it is sent and is reconciled to the reported token usage, so concurrent assertions cannot overspend (a failed call keeps its reservation; a request without an output limit is capped at 1024 tokens); a call that cannot reserve is not sent and its assertion isnot_evaluated: budget_exhausted. Prices come from a built-in table (gpt-4.1,gpt-4.1-mini,gpt-4.1-nano,text-embedding-3-small,text-embedding-3-large) orATTEST_JUDGE_PRICE_IN/ATTEST_JUDGE_PRICE_OUT(USD per 1M tokens); a USD budget with an unpriced judge or embedding model, or with theacpjudge, is a startup error. Seedocs/guides/configuration.mdanddocs/guides/judges.md. evaluate_batchresults carryusage: model calls and the judge calls among them, input, output and cached tokens, reconciledcost_usd,retry_cost_usd,unpriced_callsandbudget_refused. Report-v2 accepts it assummary.usage; the engine’s markdown report printsmodel calls · tokens (cached) · $ · retry cost. Judge results cost the same per token on every provider (Anthropic and Gemini judges previously reported $0).- Judges can abstain: built-in rubrics (now version
v2) ask for{"abstain": true, "reason": "..."}when the output lacks the evidence the rubric needs, and the assertion isnot_evaluated: judge_abstainedwith the reason. Only the structured field counts; no phrase in an explanation is matched. Abstentions are not cached, and abstainingrepeatruns and bias probes are left out. attest policy evaluate --baseline <tag>with aregression: {confidence, tolerance}rule (defaults 0.95 and 0.05) compares the pass rate of each assertion with at least 2 runs in both the report and the baseline (fixed-N trials) with the baseline using a Wilson interval; single runs keep theblock_on_regressionstatus comparison, and trial assertions it cannot compare areinconclusiveand returnsverdictpass,regressed(exit 1) orinconclusive(exit 2) with per-assertion intervals. Report-v2 rows accept a 1-basedtrialand test rows atrialscount. Seedocs/guides/verdicts.md.attest baseline pinstores each assertion’s passes, evaluated runs and not-evaluated runs, folding the rows of fixed-N trials into one entry;baseline showprints them. The baselines table in$ATTEST_CACHE_DIR/attest.dbgains the columns on first use, and rows pinned earlier count as one run.- Fixed-N trials:
pytest --attest-trials=Nruns every test that uses theattestfixture N times, and the Vitesttrials()helper (N fromnorATTEST_TRIALS) runs a test body N times. During a trialevaluate()records without enforcing; the test fails when the pass rate of its evaluated trials is below the minimum pass rate (default 1.0, so any failing trial fails it;--attest-min-pass-rate/@pytest.mark.attest_min_pass_rate(...), VitestminPassRateonattestGlobalSetuportrials(), orATTEST_MIN_PASS_RATE), not-evaluated trials are left out of the rate, and a trial that raises counts as a failed trial whose rows are replaced by onehard_failrow per assertion. Report rows carrytrialand test rowstrials;attest policy evaluate --baseline <tag>with aregressionrule turns them intopass,regressedorinconclusive. Assertion ids inside a test are derived from the test id and the assertion’s position, so they match across trials and runs. Seedocs/guides/statistical-verdicts.md. - Per-test budgets:
@pytest.mark.attest_budget(max_judge_calls=..., max_cost_usd=...)and the VitesttestBudget: { maxJudgeCalls, maxCostUsd }option ofevaluate/inspect(budgetontrials()) send the test’s remaining budget with each evaluation; agent spend frommetadata.cost_usdis checked between trials only, and further trials stop once the test budget is spent. - The pytest and Vitest run summaries print
attest usage: <calls> model calls · <tokens> tokens (<cached> cached) · $<cost> · retry cost $<cost>, and reports record it insummary.usage. The SDKs know thebudget_exhaustedandjudge_abstainedreasons. - Trace schema version 2 (
proto/attest/v1/trace-v2.schema.json, fixtures inprotocol-tests/trace-v2/): optional conversationturns, agenthandoffs,conversation_id, and per-stepretry_of,error, recorded request/responseexchange,span_id,parent_span_idandtool_call_id. The engine accepts schema versions 0–2; version 1 traces are unchanged. - Behavior-contract trace checks in the engine (Layer 3,
type: "trace"):tool_cardinality(calls per tool, optionally per argument value, reported asissue_refund called 2× for order_id=O-1042 (max 1)),trajectory(strict,unordered,subset,superset,partial_order),tool_order,cycle(period 1–4),failure_streakandarg_predicate. They match tool calls only and count retried calls. Specs are listed inproto/attest/v1/protocol-spec.md. - Behavior-contract DSL in Python and TypeScript:
tool_cardinality/toolCardinality,trajectory,trajectory_partial_order/trajectoryPartialOrder,tool_called_before/toolCalledBefore,no_tool_cycles/noToolCycles,max_tool_failure_streak/maxToolFailureStreakandtool_arg_satisfies/toolArgSatisfies. Both SDKs emit identical specs, checked byprotocol-tests/contracts/dsl.json. Seedocs/guides/behavior-contracts.md.
Changed
- The
loop_detectiontrace check shares thetool_cardinalitycounter; its failure explanation now readsfetch called 3× (max 2). Its result status is unchanged. - BREAKING:
attest calibrate --persistrequires--judge-providerand--judge-model. The calibration table in$ATTEST_CACHE_DIR/attest.dbgainsjudge_providerandjudge_modelcolumns on first use; labels stored before have neither and are not counted for any provider. - BREAKING: The Python
OTelAdapterconverts spans through the engine (attest-engine trace convert) instead of its own mapping, so it needs an engine and returns trace v2. Tool steps takeargsandresultfromgen_ai.tool.call.arguments/gen_ai.tool.call.result(previouslycall_id,parametersandoutputfrom non-standard attributes; the call id is nowtool_call_id), a top-levelinvoke_agentspan becomes a conversation turn instead of anagent_callstep, and malformed spans raiseattest.exceptions.OTLPConversionError. An engine binary is required (the SDK downloads its engine version, or setATTEST_ENGINE_PATHto one built withmake engine).OTelAdapter.from_otlp()converts an OTLP/JSON export given as apathlib.Path, JSON text, bytes or a parsed mapping. Span events, span kind and resource attributes are not read.to_otel_spans()writes the current semantic convention attributes under aninvoke_agentroot span.Trace.to_dict()omitsparent_trace_idwhen it is unset. - BREAKING: The TypeScript
OTelAdapter.fromSpans()converts spans through the engine and returns aPromise<Trace>(trace v2) instead of aTrace. Spans needspanContext().spanId;parentSpanId(sdk-trace-base 1.x) orparentSpanContext(2.x) andstatusare read.OTelAdapter.fromOtlp()converts an OTLP/JSON export, and malformed input rejects withOTLPConversionError. Invoke-agent, retrieval andexecute_toolspans, previously dropped or misread, now convert as in Python.new OTelAdapter(agentId)is removed (use the static methods), spanattributesmust be OpenTelemetry attribute values, andfromSpans([])rejects (an export needs at least one span). TheStepandTracetypes gain the trace v2 fields. - Built-in judge rubrics are version
v2(abstain instruction added), so calibration labels stored againstv1no longer match them. repeat: Njudges take the mean of the two middle scores for an even number of scored runs (previously the upper middle score).- Gemini judge usage counts thinking tokens (
thoughtsTokenCount) as output tokens, as they are billed. - Judge and embedding calls stop when the batch’s context is cancelled.
attest policy evaluate --baselinefolds the rows of fixed-N trials into one row per assertion (worst status, mean score, summed cost and duration) before computing transitions.- Judge cache entries are keyed by the whole judge prompt (rubric version, criteria and agent output) instead of the agent output alone. Two
passes_judgeassertions with different criteria on one output previously shared one cached score. Entries cached by earlier versions are misses and are judged again. - Cached judge scores are scoped by judge provider as well as endpoint, rubric and model, so two providers behind the same gateway with the same model name never share a score. Entries cached by earlier versions are misses and are judged again.
- Judge provider error messages have any echoed API key replaced by
[redacted]and control and Unicode format characters (bidi overrides, zero-width) removed before they reach results and reports. - A constraint on a trace field the trace does not record, such as
cost_underwithoutmetadata.cost_usd, explains itsnot_evaluated: missing_evidenceresult asmetadata.cost_usd absent. - The documentation website moved to
https://attest-framework.github.io/(repositoryattest-framework/attest-framework.github.io, formerlyattest-website). The release and SDK CI website dispatches target the renamed repository, andhttps://attest-framework.github.io/attest/redirects to the new root. The old/attest-website/path no longer resolves.
Removed
- The MkDocs site configuration (
mkdocs.yml) and the pages it built outsidedocs/guides/.https://attest-framework.github.io/attest/already redirects to the documentation website, which syncsdocs/guides/*.mdat each release.
Fixed
- Install instructions in the README,
docs/guides/merge-gate.md,docs/guides/framework-adapters.mdand the Python SDK README (the PyPI page) useuv add "attest-ai>=1.0.0rc1"instead ofpip install, andnpm install -D "@attest-ai/core@>=1.0.0-rc.1" "@attest-ai/vitest@>=1.0.0-rc.1". A barepip install attest-ai,uv add attest-aiornpm install @attest-ai/coreinstalls 0.9.0, the newest stable release, until 1.0.0 is published.
1.0.0-rc.1 — 2026-11-02
First release candidate, themed “Trust”: a red result means a real failure, the SDKs run only an engine of their own version, and attest init goes from an empty project to a CI merge gate without an API key.
Added
ATTEST_SIMULATION=1runs the engine with providers disabled: layers 1–4 evaluate for real, layers 5–6 reportnot_evaluated: simulation.inspect()(Pythonattest.inspect(),@attest-ai/vitestinspect()) evaluates without enforcing. Pythonattest.enforce(verdict)applies the session’s strict setting to anAgentResult,RepeatResultorScenarioResult: it fails with the failing trials’ diagnostics or skips with the not-evaluated reasons summed across trials.--attest-strict(pytest),strict: true(Vitest) andATTEST_STRICT=1turn not-evaluated skips into failures.attest init(Python and TypeScript) writes a sample test that passes with no API key: a refund agent’s run built withTraceBuilder, checked by schema, constraint, trace and content assertions, with one line markedATTEST-DEMOthat turns it red with the layer, expected and actual value. It also writesattest.policy.yaml(block on hard failures, warn on not-evaluated assertions). The Python sample test defines themy_agent_resultfixture it uses (the old sample used it without defining it), so an existingtests/conftest.pydoes not affect it.attest init --ci githubwrites.github/workflows/attest.yml: the test step continues on failure, the report upload andattest policy evaluatealways run, thepr-annotateaction always runs for pull requests from the same repository (a fork’s read-only token cannot post reviews), a policy warning (exit 2) does not fail the job, and a final step fails the job when the tests failed. Seedocs/guides/merge-gate.md.- report-v2 (
proto/attest/v1/report-v2.schema.json, fixtures inprotocol-tests/report-v2/checked in Go, Python and TypeScript): every assertion row carries thetest_id,fileandlineof its test, atestslist gives each test’s outcome (passed,failed,skipped) and not-evaluated reasons, andsummary.not_evaluatedcounts not-evaluated rows. pytest --attest-report=<path>writes report-v2 for every test in the session.AttestReporter(@attest-ai/vitest/reporter) writes report-v2 when givenoutputFilein the Vitest config, or from the command line:vitest run --reporter=default --reporter=@attest-ai/vitest/reporter --outputFile.attest=<path>.inspect()/evaluate()attach results to the running test, so they reach the reporter from Vitest workers; a retried test reports only its last attempt’s results (pytest reruns likewise).release.ymlpublishes a release from one tag push: it builds the six engine binaries, checksums, the Python distributions and the npm tarballs once, verifies them, and afterreleaseenvironment approval publishes the GitHub (pre-)release, then PyPI (trusted publishing) and npm (trusted publishing with provenance) from those stored artifacts, records which components published in the release notes, and dispatches the website sync. A tag that differs fromVERSIONfails before anything builds.workflow_dispatchwithdry_run: trueruns the whole pipeline without publishing.setup-attestaccepts arefinput: consumer repositories build the engine and install the Python and TypeScript SDKs from any attest branch, tag or commit.CHANGELOG.mdis the single changelog;scripts/release-notes.sh <version>prints one version’s section.attest-engine doctor [--json]reports the engine and protocol versions, the capabilitiesinitializewould advertise, the judge provider, model, base URL and key source (never the key), the embedding provider, simulation mode and the cache directory. JSON followsproto/attest/v1/doctor.schema.json. A configuration error exits 1 with the server’s startup message; a plain-http judge endpoint on a non-loopback host is flagged as insecure.ATTEST_JUDGE_BASE_URLpoints the LLM judge at any OpenAI-compatible endpoint.ATTEST_JUDGE_API_KEYoverridesOPENAI_API_KEYfor the judge. Seedocs/guides/configuration.md.attest-engine --helplists every command; each command is one file registered through a command registry.attestconsole script for the Python SDK (uv run attest), matchingnpx attest. Both implementattest initand forward every other command (doctor,baseline,policy,cache,calibrate,version) to a version-checked engine with arguments, output and exit code unchanged. A forwarded command must be listed by the engine’shelp; an engine that predates it (such as thev1.0.0-rcprerelease, which reports the same version but has no command registry) exits 1 before running.attest --helpand bareattestalways exit 0 and never download: they listinit, plus the engine’s commands when a matching engine is already available throughATTEST_ENGINE_PATH,PATHor the versioned cache, and otherwise print how to get one.EngineVersionMismatchError(Pythonattest.exceptions, TypeScriptengine-manager) names the engine path, where discovery found it, the version it reports and the version required.
Changed
- BREAKING: the engine reports
not_evaluatedwith a reason (provider_unavailable,provider_not_configured,simulation,missing_evidence,short_circuited) instead of failing or dropping assertions it cannot judge. Judge and embedding outages and missing keys no longer show asbroken_code. - BREAKING:
attest-engine policy evaluatewarns (exit 2) onnot_evaluatedassertions; setnot_evaluated: blockin the policy to block instead. - BREAKING (Python):
attest.evaluate()fails the test on any hard failure and skips it withnot evaluated: <reason>when an assertion could not be evaluated. - BREAKING (TypeScript):
@attest-ai/vitestevaluate()fails the test on any hard failure and skips it withnot evaluated: <reason>when an assertion could not be evaluated. - BREAKING: simulation mode in both SDKs runs the real engine with providers disabled instead of returning fabricated passes; TypeScript
simulationEvaluateBatchis removed.config(simulation=True)/config({ simulation: true })requires an engine that advertisessimulation_mode, so it can never reach a real provider. - Python diagnostics count failures, not rows, and list not-evaluated assertions with their reason.
- The
pr-annotateaction places each failed assertion at thefileandlineits report-v2 row carries (the test that requested it): an inline review comment when that line is in the pull request diff, an entry withfile:linein the review body otherwise, and an::error(hard failure) or::warning(soft failure) workflow annotation at that file and line either way. It no longer guesses line 1 from test ids in the explanation; rows without a location are listed in the review body. Its unit tests run in GitHub Config CI. When thepolicyinput is set, a policy evaluator exit code other than 0, 1 or 2 (for example 127,engine-binarynot found) now fails the step with an error naming the code; it previously counted as a pass. - BREAKING:
@attest-ai/vitestrequires Vitest 3 or later (AttestReporteruses the Vitest 3 reporter API), and its@attest-ai/corepeer range is>=1.0.0-rc; the old>=0.3.0range excluded 1.0.0 prereleases, sonpm installof the two packages failed. - BREAKING: under
test.concurrent,inspect()andevaluate()require the test context ({ context }) and throw without it, so results are never attached to another test. - The TypeScript sample test written by
attest initistests/my-agent.test.ts(wastests/test_my_agent.test.ts). - The README quickstart runs as written: install,
attest init,pytest/vitest run, the marked edit, the report and the policy gate. A test runs every README code block. - BREAKING:
ATTEST_BUDGET_MAX_COSTis renamedATTEST_MAX_SOFT_FAILURES; it always counted soft failures in one engine session, not cost. No alias. - BREAKING: with
ATTEST_JUDGE_BASE_URLset, onlyATTEST_JUDGE_API_KEYis sent to the endpoint;OPENAI_API_KEYis used only at the default OpenAI endpoint. A base URL withoutATTEST_JUDGE_API_KEY, with credentials, a query or a fragment, is a startup error. - BREAKING:
OPENAI_API_KEYin the engine’s environment now enables the OpenAI judge and OpenAI embeddings (previously onlyATTEST_OPENAI_API_KEYdid). - BREAKING: non-integer
ATTEST_EMBEDDING_CACHE_MAX_MB,ATTEST_JUDGE_CACHE_MAX_MB,ATTEST_HISTORY_MAX_ROWSandATTEST_HISTORY_MAX_AGE_DAYSnow fail startup even when no provider is configured, andATTEST_RATE_LIMIT_OPENAI[_BURST]values <= 0 fail startup instead of falling back to defaults. - BREAKING: an unknown
ATTEST_EMBEDDING_PROVIDER(anything other thanauto,openai,onnx) is rejected at startup. - BREAKING:
attest-engine <unknown command>exits 2 with usage instead of starting the JSON-RPC server. - Judge cache entries are scoped to the judge endpoint, so two endpoints serving the same model name no longer share scores; entries cached by earlier versions become misses.
- BREAKING: the SDKs cache the engine at
~/.attest/bin/<ENGINE_VERSION>/attest-engine(canonical1.0.0-rc.N, never the PEP 440 form) without a.engine-versionmarker. The legacy~/.attest/bin/attest-engineis no longer read and is left in place. - BREAKING: one discovery function per SDK (
attest.engine_manager.resolve_engine, TypeScriptresolveEngine) serves the pytest fixture, Vitest,EngineManagerand theattestCLI: explicit path →ATTEST_ENGINE_PATH→PATH→ versioned cache → download. The engine found must report exactlyENGINE_VERSION, andinitializemust report it too; otherwise the SDK raises a version-mismatch error instead of running. A mismatched engine onPATHis not skipped. The monorepobin/and./bin/fallbacks are gone; setATTEST_ENGINE_PATHto a locally built engine. - BREAKING:
attest policy evaluateand every other forwarded command exit 1 before running when no matching engine is available, instead of running whateverattest-enginewas found. - BREAKING:
attest cacheandattest calibraterun in the engine (attest-engine cache,attest-engine calibrate); their output follows the engine’s format. - The TypeScript engine download uses the
.exeasset name on Windows, matching the release assets. PATHdiscovery in the Python SDK checks only the platform binary name in eachPATHentry, skipping empty entries, so the current directory and otherPATHEXTextensions are never searched. Forwarded commands killed by a signal exit 128 + N in both SDKs.
Removed
docs/changelog.md, superseded by this file.- BREAKING:
ATTEST_OPENAI_API_KEY; useATTEST_JUDGE_API_KEYorOPENAI_API_KEY. - BREAKING:
ATTEST_JUDGE_RPMandATTEST_JUDGE_BURST; useATTEST_RATE_LIMIT_OPENAIandATTEST_RATE_LIMIT_OPENAI_BURST. - BREAKING: the deprecated Python adapter Protocols
attest.adapters.TraceAdapter,ProviderAdapterandFrameworkAdapter; subclassBaseAdapterorBaseProviderAdapter.Agent(adapter=...)takes aBaseAdapter. - BREAKING:
attest validate(Python and TypeScript) and the Pythonattest run/ bareattestpytest pass-through; runpytestorvitestdirectly.attestwith no arguments prints help.
Fixed
- Vitest workers start their own engine when the
attestGlobalSetup()client is not visible to them.
Security
- The ONNX embedding model downloads from a pinned Hugging Face revision and is rejected unless its sha256 matches; a cached model that does not match is replaced. Downloads use a unique temp file, a timeout and a 256 MiB cap, and are published 0644.
- ONNX Runtime guidance targets 1.26.0 (required by
onnxruntime_gov1.31) with published checksums;ATTEST_ONNX_RUNTIME_LIBis honored and resolved to an absolute path. - Judge base URL errors never echo the URL, so credentials in it cannot reach logs.
1.0.0-rc — 2026-04-30
Tagged, not published to registries. Engine binaries are attached to the GitHub pre-release.
Added
FailureClasstaxonomy onAssertionResult(failure_class), mirrored in both SDKs.BaselineStorefor named result snapshots; reports carry abaselinedelta block and surface the failure class.- CI workflow commands:
attest-engine baseline pin/list/show/deletesnapshots reference runs;attest-engine policy evaluatereads a merge-blocking policy file and returns tiered exit codes (0 pass, 1 block, 2 warn); the Python SDK shipsattest baseline/attest policyshims; the.github/actions/pr-annotatecomposite action posts inline review comments per failed assertion.
Known limitations
- The TypeScript admin CLI shim (
attest baseline,attest policy) is not available; the Python SDK ships them.
0.9.0 — 2026-04-30
Published: PyPI attest-ai==0.9.0, npm @attest-ai/core@0.9.0 and @attest-ai/vitest@0.9.0, engine binaries on the GitHub release.
Added
- Calibrated judge subsystem: required
Rubric.Version(built-ins pinned to v1); calibration history keyed by rubric name, version and prompt hash. repeat: Njudge runs (1–16 samples) returning the median with mean and standard deviation; cost reported as N × per-run.meta_evalremains an alias forrepeat: 3.- Bias probes (
verbosity,position,self_preference, orall) that report deltas from controlled mutations of the agent output. - Calibration sets: CSV and JSONL human-label loaders and
attest-engine calibrate/attest calibrate(Python and TypeScript) producing identical Cohen’s κ, agreement and ROC-AUC. - SQLite-backed
CalibrationStorealongside the assertion-history store. - OpenTelemetry GenAI parity: current span names (
chat,text_completion,embeddings,invoke_agent,execute_tool, retrieval) with attribute-driven dispatch;gen_ai.agent.name/gen_ai.agent.iddriveTraceTreeparent/child resolution;TraceBuilder.export_otel()emits OTLP-compatible spans.
Changed
- Custom rubrics registered without
Versionnow fail at registration time.
0.8.0 — 2026-04-30
Tagged, not published to registries. Engine binaries are attached to the GitHub release.
Added
- Hierarchical Markdown report: summary header (counts, cost, P50/P95 latency), per-assertion failure blocks and a per-layer breakdown.
- JSON report v2 (
report_version: 2) with per-layer indices, latency percentiles and asimulatedflag. The v1 emitter remains available throughReportOptions{Version: ReportVersionV1}. - Diagnostic fields on
AssertionResult(Layer,Type,TraceNodePath,Expected,Actual,SuggestedAction) andJudgeMetadata(model, rubric, prompt hash, sample scores, mean, standard deviation, high-variance flag), mirrored in both SDKs. - Python:
attest.cli.diagnosticsrenderers,AgentResult.render_diagnostics(), automatic diagnostic output in the pytest terminal summary and the--attest-diagnosticsflag. - TypeScript:
diagnostics.tsrenderers;AttestCostReporterrenders diagnostics for failed runs.
0.7.0 — 2026-04-30
Tagged, not published to registries. No GitHub release was created.
Added
- Protocol diagnostic ring buffer and desync rejection in the Python and TypeScript SDKs.
- Cross-SDK protocol conformance fixture suite under
protocol-tests/. - Per-provider rate-limit configuration with a typed rate-limit error.
- Results surface dynamic threshold degradation.
Changed
- Bounded L5/L6 evaluation, strict environment-variable parsing and a simulation marker on results.
- Listener exceptions are isolated from the protocol reader.
Earlier releases
Versions 0.1.0 through 0.6.0 are described in their GitHub releases; 0.6.1 exists only as a tag. 0.1.0–0.6.0 were published to PyPI; 0.3.0–0.6.0 were also published to npm.