Changelog¶
All notable changes to agent-attest are documented here. This project follows
Keep a Changelog and Semantic Versioning.
[Unreleased]¶
Added¶
VerdictPolicy(min_claim_coverage=...): a run whose faithfulness check covered less than this share of the answer's claims goes to review. Off by default; kept separate from the score.Report.claim_coverage: the share of claims faithfulness checked (0 to 1, orNone).ProviderConfigError, raised when the provider rejects the API key (401), the key lacks permission (403) or the model doesn't exist (404). It stops the evaluation instead of becoming review results, never includes the key or the provider's response in its message, and is printed by the CLI as aprovider_configerror (exit 1).- Property-based tests (Hypothesis, dev only) for evidence escaping, error recovery,
dispositions and claim completion, including that a stricter
VerdictPolicynever gives a weaker verdict and that a finding nothing checked is never weaker than its rule. --max-claims Nonattest runandattest review, andmax_claims=onAttest.review(), set how many claims of each answer or output are checked (default 40). A negative value is invalid input (exit 2 on the command line,ValueErrorin Python).CheckResult.dispositions: each finding's disposition, the assigned one or else the one the defaultVerdictPolicy()gives it.- Benchmark: ten draft-labelled cases converted from τ-bench's public gpt-4o runs, with the converter that regenerates them (
benchmarks/converters/tau_bench.py). - Benchmark: 10 draft-labelled prompt_injection and tool_use cases converted from public
AgentDojo runs (
benchmarks/converters/agentdojo.py). - Benchmark: 12 draft-labelled ToolBench cases (faithfulness, tool use, recovery) converted from
vendored, contact-redacted upstream samples by
benchmarks/converters/toolbench.py. - Benchmark:
annotate.py settle --replacecorrects a settled entry by appending one that supersedes it, so the adjudication log stays append-only. - Benchmark:
compare.pyreports deltas per origin and fails on a regression in any origin with at least 10 cases in both reports, not only on the aggregate.
Changed¶
- Breaking:
Report.passednow means the run passed:verdict == "pass". It used to mean every check passed, so a run needing review could readpassed: true. The old meaning isReport.checks_passed. Inattest run's JSON,passedcounts runs whose verdict ispassand the newchecks_passedcounts the old way. - Breaking: the CLI's JSON is now
schema_version"2", becausepassedchanged meaning. The schemas are published underschemas/v2/; the v1 schemas stay where they were. - Breaking: claims past
max_claims(40 by default) are reportedunverifiablewithmetadata["checked"]false, so they go to review. A long answer used to pass on its first 40 claims while the rest were never checked. Inattest review, which grades each output's claims the same way, an output with more than 40 claims is now dropped as unverified; raise the cap with--max-claims. The summary says how many claims went unchecked, or that none were checked whenmax_claimsis 0. - A
Reportbuilt by hand withoutevaluate(), whose findings have no disposition, now gets the verdict (andpassedandblocking) the defaultVerdictPolicy()would give it. It used to judge each finding by severity alone, so a faithfulness result whose claims were allunverifiable, awarnpolicy finding or an ignored tool error passed; they now go to review, and a finding nothing checked (unchecked, ormetadata["checked"]false) goes to review too. - A finding nothing checked (
unchecked, ormetadata["checked"]false) gets the stricter ofreviewand the disposition its check's rule gives, in every check and under everyVerdictPolicy. It used to pass in a custom check unless its severity wasfail, and a stricter rule could make it weaker: underon_injection_warning="block"an unchecked injection warning gotreview; it now blocks. An uncheckedfailfinding blocks.on_inconclusive_faithfulnessis the rule for unchecked faithfulness claims, so setting it topassstill lets them pass. max_claimsbelow 0 raisesValueError.- No failed or missing evaluation turns into a pass. When claim extraction, verification, the
role, deep injection, tool-choice or policy judge, or a code review injection confirmation
fails, the affected findings are inconclusive (
unchecked, orunverifiablewithmetadata["checked"]false) with the error inreason, and go to review. Budget stops and provider configuration errors still stop the evaluation. - Rate limits, timeouts and server errors (408, 409, 429, 5xx) are retried before a call fails.
A
Noneor wrongly typed structured response is retried like an empty one. - A claim whose check failed or got no verdict is
revieweven beside supported claims;metadata["claims_unchecked"]counts them. - Breaking: a run with no system prompt gets an
uncheckedrole result (review) instead of passing, since there is no role to check against. - Breaking:
allowed_tools=[]allows no tools;Nonestill allows every tool. - Code review: an issue whose grading failed is dropped in every gate mode, and a failed
injection confirmation leaves the hit unresolved, so the scan is never
clean. - The CLI prints a JSON error instead of a traceback for files that aren't valid UTF-8
(
invalid_input) and for errors no check absorbs (command_failed).
Deprecated¶
Finding.effective_dispositionwarns withDeprecationWarning. It doesn't know the finding's check or result, so it can disagree with the verdict, for example passing awarnpolicy finding or a loneunverifiableclaim the default policy sends to review. UseCheckResult.dispositionsorReport.verdict.
Fixed¶
- A usage error, such as an unknown option, a missing argument or
--max-claims abc, prints the JSONinvalid_inputerror on stdout (exit 2) for every command, instead of Click's usage box on stderr. - A custom verifier that answered one claim twice and skipped another no longer hides the skipped claim.
- Benchmark: a case whose check left part of it unchecked (for example, the deep injection check failed for want of an API key) is recorded as errored, not graded as a clean label.
- Benchmark:
annotate.py settlerefuses entriesmergewould reject: a label the item can't take, a malformed--byname, a blank reason, or a discussion not between the case's current annotators. - Benchmark: a settled discussion goes stale, and is reported, when the annotators' labels for the item change on the same run.
- Benchmark:
settlelocks the adjudication log while it appends, andmerge --writereplaces each case file atomically. - Benchmark: a task family nested under a family in another split, such as
tau-bench/airline/25-49besidetau-bench/airline, is rejected as a split leak. - Benchmark: a short or repetitive
ATTEST_BENCH_ID_KEYgets a warning. - Benchmark: in private mode,
annotate.pychecks only the paths each command reads or writes, sosettlewith an owner ruling no longer needs--labelsoutside the repository.
[0.8.1] - 2026-10-31¶
Fixed¶
- An answer whose claims couldn't be checked against any evidence no longer passes. A
faithfulness result with claims but none checkable now gets the
reviewdisposition, so the report's verdict isreviewinstead ofpass(VerdictPolicy(on_inconclusive_faithfulness=...)changes it). Answers with no claims still pass. - A custom
verifyfunction that returns fewer verdicts than claims no longer makes the answer look verified: each claim without a verdict becomesunverifiable. Before, a verifier that returned nothing scored 1.0 and passed.CheckResult.passedandReport.passedare unchanged, so they don't account forreview: gate onverdict. Every CI example in the docs now does.
Added¶
- Faithfulness results record
claims_checkedandclaims_total(every extracted claim, including any past the claim limit) inmetadata, so a pass shows how much was checked. attest runreportsverdicts: how many runs gotpass,reviewandblock.- A Limitations page: what attest assumes (authentic, complete runs), where its checks rely on heuristics, and how to use its results safely.
Deprecated¶
ToolUsePolicy(on_invalid_arguments=...)has no effect, because no check reports invalid arguments; setting it now raises aDeprecationWarning. It will be removed in a future minor release.
[0.8.0] - 2026-10-31¶
Added¶
- Finding dispositions: a
VerdictPolicy(withToolUsePolicy) gives each finding adispositionofpass,revieworblock, andReportgainsverdict(the strictest disposition),blockingandscores, so a security failure can't be averaged away. PassAttest(verdict_policy=...)to change the defaults;passedandoverall_scoreare unchanged.
Changed¶
attest runandAttest.evaluateask whether the final answer admits an unrecovered tool error whenever tool use is checked and a model key is available, instead of only with--appropriate. Without a key the error staysunchecked, as before. A failed answer check is reported asuncheckedwith its reason rather than stopping the run.PROMPT_VERSIONis now2026.10.31(artifact evidence blocks, the tool-error answer question).- Faithfulness now treats artifacts as evidence: artifact text is shown to the grading model in
its own escaped
<artifact>block, and findings record the block their quote came from inmetadata["location"]. Binary and path artifacts are extracted only withAttest(extract_artifacts=True)(plusallow_paths=Truefor paths); files cut short are markedcomplete="false". Runs without artifacts render their evidence exactly as before. - Breaking: the tool-use check no longer counts a tool error as handled because any later
tool call happened. Only a successful retry of the same tool or a declared fallback
(
Trajectory.tool_fallbacks) recovers it; otherwise one model call reads the final answer (acknowledged,silentorclaimed_success), or the error isuncheckedwhen no model is available. Verdicts are nowcorrect,disallowed_tool,error_silent,error_claimed_successandincorrect(tool choice); finding metadata replaceserror_handledwithrecoveryandrecovered_at. NewToolCall.is_erroroverrides the error guess, andattest tools --check-answer/tool_use(check_answer=True)read the answer without the tool-choice check.
[0.7.0] - 2026-10-31¶
Added¶
from_haystack_messagesconverts a Haystack 2.xAgentrun (ChatMessageobjects or theirto_dict()form) into aTrajectory, pairing each tool call with its result.- Opt-in response caching via
Attest(cache=True)(or a shared dict), so re-evaluating an identical run reuses judge calls without collapsingvoteself-consistency. - Spend controls:
Attest(budget=Budget(max_calls=..., max_tokens=...))fails closed withBudgetExceededbefore a call that would pass the limit; everyReport.metadata["usage"]andAttest.usagerecord calls, input/output tokens, and cache hits. - Persistent response cache:
Attest(cache="path/")stores verdicts on disk viaDiskCache, namespaced by provider and model, so reruns across processes reuse them. - Provider prompt caching: shared context (faithfulness evidence across claim batches, the task and
tool list across tool-choice calls, the whole prompt across
votesamples) now leads the prompt so Gemini and OpenAI implicit caches reuse it, with explicitcache_controlbreakpoints on Anthropic only when the prefix will be resent.Usagereportscached_input_tokensandcache_write_tokens. attest run --max-calls/--max-tokens/--cache-dir, withusagein the output; exits with code 3 when the budget stops a run.- Opt-in real-LLM integration tests behind the
integrationmarker andATTEST_INTEGRATION=1. Attest.review(pr, outputs)andattest.code_review.review(...)check and gate every output of a code review in one call. The result,ReviewResult, has oneOutputDecisionper output (text,keep,mode,score,reasons,notes,omissions), plus the guidelines used,kept,droppedandfilter(). Outputs without their own diffs use the pull request's.- Team review rules: pass
guidelines="REVIEW.md"(or several files, or markdown text) toreview(). Each list item in the markdown is a rule. The rules are added to every output's evidence, so an issue that cites one can be graded, and a newguidelinescheck drops any output that breaks a rule (breaks_guideline). An output that only leaves out something a rule asks for is kept with amisses_guidelinenote. Each finding must quote the output exactly, and each break is confirmed by a second call that asks separately whether the output reports, approves or itself does what the rule forbids; reporting rule-breaking code is never a break.follow_guidelines=Falseturns the check off. Without guidelines, outputs are checked for faithfulness only, and attest's own reviewer follows a built-in general standard. - attest can write the review itself:
Attest.write_review(pr, guidelines=...), orreview(pr)with no outputs, produces issues, one summary per changed file and an overview, then checks them like any reviewer's output. It always follows the general standard as a baseline; team rules add to it and win where they conflict. attest review FILEreads{"pr": ..., "outputs": [...]}and prints one decision per output. Leave outoutputsto have attest write the review.--guidelines/-g PATH(repeatable) passes review rules,--no-followskips the rule check, and--mode TYPE=MODEoverrides a gate mode.attest schema [COMMAND]prints the JSON Schema of a command's output. The schemas are also published athttps://peace-orefejo.github.io/attest/schemas/v1/.answer_kindaccepts"code"and"auto"."auto"picks the grading standard from the run and records the choice in the report metadata.- Two new default injection detectors: hidden Unicode (bidi controls, zero-width and tag characters) and encoded payloads (base64 and hex that decode to instructions).
- Instance-bound evaluation policies with YAML/JSON loading, deterministic text/tool rules, evidence-grounded and rubric rules, automatic enforcement, and stable policy identity hashes.
- Bounded extraction of JSON, CSV, XML, HTML, PDF, image OCR, image metadata, and QR content for
prompt-injection scanning through the optional
filesdependency bundle. --extract-files/--no-extract-filesand--allow-pathsflags on theinjectioncommand, and matchingextract_files,allow_paths, andextraction_limitsoptions onAttest.injection.- Total detection cap (
ExtractionLimits.max_detections) that bounds custom detector output and terminates unbounded or infinite detector iterables, emittingunscanned_content. examples/pdf_agent.pyend-to-end demo evaluating a PDF-reading agent for injection resistance, including "held" versus "compromised" trajectories.
Changed¶
- Curated model lists and defaults updated. Defaults:
claude-haiku-5-5(wasclaude-haiku-4-5),gpt-6-luna(wasgpt-5.6-luna),gemini-3.8-flash(wasgemini-3.6-flash). Addedclaude-fable-5-1, the GPT-6 / GPT-6.1 family andgemini-4-argon; dropped retired or superseded models such asgemini-2.5-pro. Any model id can still be passed withmodel=. - License: attest is now licensed under the GNU Affero General Public License v3.0
(
AGPL-3.0-only). Versions 0.6.0 and earlier remain available under the MIT License. - Breaking: every CLI command prints JSON, and only JSON. The text view and the
--jsonflag are gone. Each command prints one envelope,{"schema_version": "1", "command": ..., "ok": true, "data": ...}, and a command that can't run prints"ok": falsewith anerror(code,message) on stdout instead of text on stderr. Exit codes: 0 ran, 1 missing API key, 2 invalid input, 3 stopped by a budget.datachanges shape from 0.6.0's--json:tools,injectionandrolewrap their list intrajectories,demoincomparisons, andrunaddstotal,evaluatedandpassed.richis no longer a dependency. - Faithfulness and tool-choice prompts put the shared evidence before the per-call text;
PROMPT_VERSIONis now2026.10.29. votestops sampling once a verdict holds a strict majority (2 of 3 agreeing skips the third call), cutting self-consistency spend by up to a third with identical verdicts.- The LangGraph adapter discriminates messages by their stable
BaseMessage.typerole instead of the class name, so a LangChain class rename no longer breaks it silently. - Split the monolithic
extractionmodule into anextraction/package (limits,pdf,image,markup,tabular,core) behind an unchanged public API. - Codebase reorganized for readability behind an unchanged public API: prompts live in a shared
prompts/package, LLM plumbing inllm/, shared helpers inutils/, andpolicy,checks.injectionandcliare now packages of small modules. ExtractionLimits,ExtractionOutcome,RegexPattern,BudgetandUsageare Pydantic models. Invalid values raise pydanticValidationError(aValueError).Severityis aStrEnum, sostr(Severity.PASS)is"pass".Trajectory.tool_calls()returnsIndexedToolCall(step, call)named tuples; unpacking still works.- Tests live next to their modules as
<module>_test.py; ruff and strict pyright are configured. - Breaking:
Report.overall_scoreisNonewhen no check produced a score, instead of 1.0. - Breaking:
Trajectory.evidence()wraps each tool call in a<tool_output index=... name=... arguments=...>block and escapes any such tag inside an output. Before, blocks were plain[i] name(args) -> outputlines, so a tool output (a web page, a file, a diff) could fake another tool's result. Every model call is told how the blocks work.PROMPT_VERSIONis now2026.10.30. - Every command's terminal output now uses the same structured layout.
GateDecisionsplits its findings:reasonsare what dropped the output, andnotesare findings shown alongside it that didn't, such as a suspicious hit or a compromised finding that affected a different output.- A payload or suspicious hit in the pull request's title, description or commit messages now applies to every output. Before, no output matched its location, so it never dropped or noted anything.
review()raisesValueErrorfor adiff_summarywith no diffs of its own when the pull request changes several files, since a summary describes one file.ReviewResult.warningsflags a pull request that changes the file its review rules were loaded from.attest run'smean_overall_scoreleaves out reports with no overall score.python -m attest.cliworks again after the CLI became a package.- The code injection summary lists only the verdicts found, e.g. "3 hits: 1 payload, 2 suspicious."
- The
codeanswer kind's verifier reads aread_guidelinesevidence step as the team's rules: authoritative about what the team requires, never about what the code does. - The claim extractor keeps an answer's bottom line as its own claim and receives the task as context, so a conclusion can't be reworded to fit the agent's own justification.
- Anthropic grading now uses native structured outputs (
messages.parse) instead of instructor's forced tool call, so Claude 5.5-generation models such asclaude-sonnet-5-5work. - Sampling parameters are sent only to Anthropic models that accept them (Claude 4.6 and older);
Claude Opus 4.7+ and the Claude 5 family run without
temperature. - Dependency bounds:
anthropic>=0.77,<2andinstructor>=1.15.3,<2. - Added
claude-opus-5-5,claude-sonnet-5-5, andclaude-haiku-5-5to the curated Anthropic model list. - Refreshed the provider model registry: Anthropic Claude 5 family, OpenAI GPT-5.6, and Gemini 3.x defaults and curated model lists.
- Disabled-extraction warnings are capped per artifact and summarized with an omitted count, so a large binary-artifact trajectory can no longer emit unbounded findings.
Fixed¶
attest demono longer crashes when the grounding score is unavailable.- Hard provider errors (auth, billing, bad request) are no longer retried as "empty responses":
callraises the provider's own error after one attempt instead of three retries ending in a misleading "no parseable content" message. Empty candidates and unparseable output still retry. - An answer whose claims were all unverifiable scored 100% ("0/0 grounded"). It now has no score and says none of its claims could be checked.
- Fresh installs that resolve
anthropic1.x andinstructor1.17 no longer fail every LLM call withMessages.create() got an unexpected keyword argument 'temperature'. - A
.envfile in the working directory (or a parent) is now read by both the library and the CLI, but only for provider API keys that aren't already set. Other variables in it, such as a base URL that would redirect your key, are ignored. - Terminal output shows control and bidi characters from tool outputs as visible escapes, so untrusted text can't clear the screen, hide lines, change colors or write to the clipboard.
- Duplicate keys in policy YAML are caught after YAML resolves them, so
true:andTrue:no longer overwrite each other silently. - Diff parsing keeps every line inside a hunk as content, so an added line shaped like a file header can't start a fake file and hide later lines from the injection scan.
- A
@generatedmarker or a very long line added by the pull request no longer hides that file from the grader and the reviewer. - Code injection scanning: a quoted "example" no longer discounts a high-confidence payload outside test and doc files; hitting a match or decode cap marks coverage incomplete; and the shell, Ruby, PHP, Java and Rust lexers recognise comment and string forms they used to miss.
- Scans that could take quadratic time on crafted input (hex runs, Python docstrings, unclosed HTML comments in review rules) are now linear.
policy.pyno longer uses Python 3.12-only f-string syntax, restoring the declared 3.11 floor.- Resolved static type errors across the injection, extraction, policy, API, and provider modules.
0.6.0 - 2026-07-30¶
Added¶
- Machine-readable
--jsonoutput for therun,tools,injection, androlecommands. - Provenance-aware
Artifact,TextSegment, andDetectionmodels for untrusted content. - Extensible named regex detectors with categories, confidence, severity, bounded execution,
match offsets, custom JSON pattern packs, and
--patterns-only. - Injection coverage metadata and a measured benign/malicious regression corpus.
Changed¶
- API clients are created lazily, so deterministic tool-use and shallow injection checks run without an API key or provider client.
- Deep injection evaluation always checks novel payloads while receiving only a bounded, deduplicated, escaped, and explicitly delimited view of untrusted content.
- Low-confidence and educationally quoted matches are reported as
possible_injectionrather than being suppressed.
Fixed¶
- Invalid, empty, or duplicate check selections and unsupported answer kinds now fail loudly instead of allowing an empty report to pass with a perfect score.
- Malformed trajectory files now report concise filename and JSONL line-number errors.
- User-uploaded artifact text can no longer enter the faithfulness evidence set.
- Catastrophic custom regular expressions are stopped by mandatory execution timeouts and incomplete scans report the attempted and skipped pattern counts.
- Attacker-provided deep-context delimiters are escaped so they cannot forge trusted prompt sections.
0.5.1 - 2026-06-25¶
Judge precision and reproducibility. These changes were driven by red-teaming a live agent (a civic assistant) whose trajectories exposed two false-positive patterns in the checks.
Fixed¶
check_role_adherencenow sees the agent's tool calls and their outputs. The judge previously received only the system prompt, task, and final answer, so any policy that conditions behaviour on tool use (e.g. "only state a phone number a tool returned") was judged blind and false-flagged compliant answers asout_of_scope_complied. The tool calls and outputs are now part of the judge's context.check_role_adherencehonours explicit policy carve-outs. A policy with a headline domain plus named exceptions (safety, emergencies, first aid, helplines) no longer reads those exceptions as out of scope; honouring a behaviour the policy explicitly permits isin_scope.check_injectiondeep mode no longer flags the agent for answering the user's own request. Returning information a tool surfaced, or retrieving to fulfil the user's task, is authorized even when the same tool output also carried injected instructions. Only acting on a new, unrequested goal counts as a compromise.
Added¶
- Self-consistency voting (
attest._llm.vote). The verdict checks (check_role_adherenceandcheck_injectiondeep) now sample the judgesamplestimes (default 3) and take the majority verdict, which stabilises non-deterministic providers. Configurable per call viasamples=.
Changed¶
- LLM calls are pinned to
temperature=0at the chokepoint for reproducibility. PROMPT_VERSIONbumped to2026.06.25to reflect the revised role and injection prompts.