assay evidence¶
Manage Assay evidence bundles and external evidence imports.
Synopsis¶
Receipt Schema Registry¶
Inspect and validate the machine-readable receipt schema registry:
assay evidence schema list
assay evidence schema show promptfoo.assertion-component.v1
assay evidence schema show promptfoo.assertion-component.v1 --raw
assay evidence schema validate \
--schema promptfoo.assertion-component.v1 \
--input receipt.json
For JSONL importer inputs, validate each non-empty row with --jsonl:
assay evidence schema validate \
--schema promptfoo-cli-jsonl-component-result.v1 \
--input results.jsonl \
--jsonl
The schema CLI covers the current receipt schema registry:
- receipt payload schemas for Promptfoo, OpenFeature, CycloneDX ML-BOM, Mastra, Pydantic, and LiveKit receipts
- importer input schemas where the reduced input artifact differs from the receipt payload
- metadata such as schema
$id, family, status, source path, short description, and Trust Basis claim when one exists
Mastra, Pydantic, and LiveKit remain importer-only in this registry. They have input and receipt schemas, but no public Trust Basis score, case-result, or acted-family receipt claims yet.
validate exits 0 when the artifact matches the selected schema and exits 1 when the artifact is valid JSON/JSONL but fails schema validation. Invalid JSON, invalid JSONL rows, empty JSONL input, unknown schema names, unreadable files, and runtime/configuration errors remain input/config errors (2+).
Options¶
| Command | Description |
|---|---|
assay evidence schema list [--format text|json] | List all supported schema entries |
assay evidence schema show <SCHEMA> [--format text|json] [--raw] | Show schema metadata or raw JSON Schema |
assay evidence schema validate --schema <SCHEMA> --input <PATH> [--jsonl] [--format text|json] | Validate a JSON or JSONL artifact |
Schema names can be the registry name, known alias, source path, or JSON Schema $id. Use list to discover supported names.
MCP Execution Record Pairing¶
Verify that request binding material and server execution-record fixtures pair up from the consumer side:
assay evidence verify-mcp-records \
--attestation sep2787-attestation.json \
--decision server-decision-record.json \
--outcome server-outcome-record.json \
--format json
For deployments without SEP-2787 attestation, supply the observed tools/call params plus _meta request envelope instead:
assay evidence verify-mcp-records \
--request-envelope tools-call-envelope.json \
--decision server-decision-record.json \
--outcome server-outcome-record.json \
--format json
--attestation and --request-envelope are mutually exclusive. Exactly one is required.
This command emits an assay.mcp.execution-record-pairing.report.v0 report. It computes the binding digest, checks the decision and optional outcome backLink fields, verifies the outcome's decisionDigest commitment to the full signed decision record, and verifies the narrow decision/outcome enum surface. In SEP-2787 mode the binding digest is the attestation JCS digest and the nonce comes from issuerAsserted.nonce. Request-envelope mode defaults to --fallback-projection whole-envelope, which digests the supplied envelope. With --fallback-projection named, params must be a present JSON object and the digest binds only {projection, params, binding}, where binding is _meta.authorization_binding. Missing or non-object params fails closed with exit 2, a stable fallback_projection_*_params check id, and binding.digest: null. The nonce is only checked for decision/outcome record consistency in either request-envelope mode.
The command is deliberately not an MCP proxy, issuer, policy engine, or runtime truth oracle. It does not verify signatures, establish issuer key trust, prove policy correctness, prove side effects, or disclose payload/result bodies. It is for downstream verifier fixtures and reviewer-visible pairing diagnostics. Request-envelope fallback does not prove the server observed that envelope honestly, or that the server-chosen nonce was unique or fresh for the call.
If --outcome is omitted, Assay reports a valid decision-only pairing check. Pairing or enum mismatches produce a report and exit 2.
Verify Side Effects¶
Promote observed side-effect assertions using imported provider audit records, then project what claims each call can support:
assay evidence verify-side-effects bundle.tar.gz --format json
assay evidence verify-side-effects bundle.tar.gz --audit-import ./audit --format table
The command emits assay.side_effect_verification.v0.
An audit record binds a call's action shape (action class, verb, target), not a call: the v0 record carries no call identity. Records are read in file-name order, and a file whose content repeats an earlier one (compared in RFC 8785 canonical form, so key order, whitespace and 1 / 1.0 do not matter) counts once (audit_records_duplicate). When several calls share one shape:
- with at least as many distinct records as calls, every call is promoted and carries
allocation: {outcome: shape_bound, calls, records}; which record went to which call means nothing, and records beyond the number of calls count asaudit_records_unmatched(the provider logged more effects of that shape than the bundle observed calls), as they do when a shape has one call; - with fewer records than calls, no call is promoted, each carries
allocation: {outcome: ambiguous, calls, records}, and the records are counted inaudit_records_ambiguousrather than inaudit_records_unmatched.
Each call also reports the decision the surface recorded for it (decision_effect: allow, deny, another value as given, or unknown). A call whose recorded decision is deny (in any letter case; other values such as denied are reported as given and not treated as a denial) and that still asserted a side effect carries decision_conflict: {effect, enforced} and is counted in decision_conflicts. Its level and claims are left as they are: the conflict is reported, and the execution is not dropped because a denial should have prevented it.
For each call, two claim fields are emitted:
occurrence_claim- what can be claimed about "this effect happened"bounded_negative_claim- what can be claimed about "this effect did not happen"
When either claim is not allowed, its reason is now emitted in a sibling field:
occurrence_reasonbounded_negative_reason
Reason shape:
- claim-gate qualification:
{ "origin": "claim_gate", "gap": "<gap>", "rule": "<rule>" } - observer refutation override:
{ "origin": "observer_refutation" }
In table output, non-allowed claims include reason=<gap> (or reason=observer_refutation) so the qualification is visible without reading JSON.
MCP Tunnel Observed-Facts Check¶
Validate one bounded MCP tunnel observed-facts fixture and classify its join evidence from the consumer side:
assay evidence verify-mcp-tunnel-observed \
--artifact examples/mcp-tunnel-observed-evidence/fixtures/valid.tunnel.json \
--format json
This command emits an assay.mcp.tunnel-observed.report.v0 report. It checks the provider-neutral assay.mcp.tunnel_observed.v0 fixture shape, enforces the no-raw-payload and no-raw-authorization boundaries, and reports whether evidence_refs form a strong same_request_instance join or only diagnostic correlation.
A strong join requires the referenced evidence to bind the same request_envelope_digest and request_envelope_canonicalization as the tunnel artifact. Route, upstream, request id, timestamp, or provider request id alone remain diagnostic correlation. The command deliberately does not prove tunnel mediation, agent identity, authorization success, policy correctness, tool result truth, application outcome truth, or issuer/key trust. Boundary violations produce a JSON/table report and exit 2.
CycloneDX ML-BOM Model Import¶
Import one selected CycloneDX ML-BOM machine-learning-model component into a verifiable Assay evidence bundle:
assay evidence import cyclonedx-mlbom-model \
--input bom.cdx.json \
--bundle-out cyclonedx-model-receipt.tar.gz \
--source-artifact-ref bom.cdx.json
The importer is intentionally strict in v1:
- input must be CycloneDX JSON with
bomFormat = CycloneDX - model components must live in
components[] - the selected component must have
type = machine-learning-model - the selected component must have bounded
bom-refandname - if multiple model components exist,
--bom-refis required - full BOM graphs,
modelCardbodies, dataset bodies, vulnerabilities, licenses, pedigree, metrics, and fairness/ethics sections are excluded
The importer first computes source_artifact_digest over the full BOM file, then reduces the selected model component. Receipts stay small while still binding back to the exact source artifact bytes.
The receipt is an inventory-boundary artifact. It does not mean the model is safe, approved, licensed, compliant, vulnerable or non-vulnerable, fair, or correct. It also does not import full CycloneDX BOM truth into Assay.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
Trust Basis emits external_inventory_receipt_boundary_visible when the supported CycloneDX ML-BOM model-component receipt shape is present. That claim means the bounded inventory receipt boundary is visible; it does not mean the BOM is complete, the model is safe, the model card is correct, the datasets are approved, or the CycloneDX artifact is imported as Assay truth.
Use --bom-ref <REF> when the BOM has multiple machine-learning-model components. Use --import-time <RFC3339> for deterministic fixture generation.
Options¶
| Option | Description |
|---|---|
--input <PATH> | CycloneDX JSON BOM artifact file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--bom-ref <REF> | Select a machine-learning-model component by bom-ref |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
Mastra ScoreEvent Import¶
Import bounded, reviewer-safe Mastra ScoreEvent / ExportedScore-derived score artifacts into a verifiable Assay evidence bundle:
assay evidence import mastra-score-event \
--input mastra-score-events.jsonl \
--bundle-out mastra-score-receipts.tar.gz \
--source-artifact-ref mastra-score-events.jsonl
The importer is intentionally strict in v1:
- input must be JSONL with one reduced score-event artifact per row
- each row must use
mastra.score-event.export.v1 - each row must use
surface = observability.score_event scoremust be numerictarget_refand at least one scorer identity (scorer_idorscorer_name) must be presentscore_id_refmaps to MastrascoreIdwhen present; it is live-backed on@mastra/core1.29.1/@mastra/observability1.10.2, but remains optional in the v1 reduced artifact for older captures and compatibility fixturesscore_source,trace_id_ref,span_id_ref, andscore_trace_id_refare reviewer aids only, not receipt identity in v1- raw exporter callback payloads, raw
metadata, rawcorrelationContext, trace trees, spans, logs, metrics, feedback, prompts, request/response bodies, scorer configs, and dashboard state are excluded
The importer first computes source_artifact_digest over the full JSONL file, then parses and reduces score-event artifacts. Receipts stay small while still binding back to the exact reduced source artifact bytes.
The receipt is a score-boundary artifact. It does not mean the score is correct, the scorer is reliable, the Mastra runtime behaved correctly, the trace/span anchor is complete, or the score should pass or fail a gate.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
P14c does not add a Trust Basis claim. The first Mastra compiler slice proves the receipt bundle is bundleable, verifiable, and readable by the Trust Basis path. P14d freezes the current compatibility decision: score receipts remain importer-only until any future score-receipt Trust Basis claim has an explicit claim boundary, Trust Card impact, and Harness posture.
Use --import-time <RFC3339> for deterministic fixture generation.
Options¶
| Option | Description |
|---|---|
--input <PATH> | Mastra reduced ScoreEvent JSONL artifact file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
LiveKit Tool Action Import¶
Import bounded LiveKit FunctionToolsExecutedEvent-derived artifacts into a verifiable Assay evidence bundle:
assay evidence import livekit-tool-action \
--input livekit-tool-action.json \
--bundle-out livekit-tool-action-receipts.tar.gz \
--source-artifact-ref livekit-tool-action.json
The importer is intentionally strict in v1:
- input may be one JSON object, a JSON array of objects, or JSONL rows using
livekit.function-tools-executed.export.v1 - each artifact must use
framework = livekit_agents - each artifact must use
surface = function_tools_executed - each artifact must use
runtime_mode = agent_session - optional
typemust befunction_tools_executedwhen present - one receipt is emitted per function call / output pair
- calls and outputs are paired by LiveKit SDK list order
- if every paired call/output entry has
call_id, mismatches fail the import as an audit consistency check - partial
call_idpresence is accepted and still uses list-order pairing - missing
FunctionCallOutput/nulloutput entries are preserved ascompleted=falsewithout inferringis_error - raw tool arguments and outputs are accepted only as fixture input for hashing; receipts store
arguments_hash/output_hashor explicit reviewer-safe refs - transcripts, audio, user input, model output, room state, participant identity, usage telemetry, latency telemetry, capture context, session identity, full traces, and spans are excluded
The importer first computes source_artifact_digest over the full reduced artifact file, then reduces each function tool action. Receipts stay small while still binding back to the exact source artifact bytes.
The receipt is an acted-boundary candidate artifact. It does not mean the tool call was correct, intended, allowed, safe, or representative of the full LiveKit session. It also does not claim LiveKit endorsement or a stable LiveKit wire contract.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
assay trust-basis generate livekit-tool-action-receipts.tar.gz --out livekit-tool-action.trust-basis.json
P47 Stage 1 does not add a Trust Basis claim. LiveKit tool-action receipts remain importer-only until any future acted-family claim slice defines exact semantics, Trust Card impact, family-matrix posture, and compatibility rules.
Use --import-time <RFC3339> for deterministic fixture generation.
Options¶
| Option | Description |
|---|---|
--input <PATH> | LiveKit reduced function-tool execution artifact file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
OpenFeature Details Import¶
Import bounded OpenFeature boolean EvaluationDetails artifacts into a verifiable Assay evidence bundle:
assay evidence import openfeature-details \
--input openfeature-details.jsonl \
--bundle-out openfeature-decision-receipts.tar.gz \
--source-artifact-ref openfeature-details.jsonl
The importer is intentionally strict in v1:
- input must be JSONL with one bounded
EvaluationDetailsartifact per row - each row must use
openfeature.evaluation-details.export.v1 - each row must represent
target_kind = feature_flag result.valuemust be booleanresult.reasonis a bounded string, not an Assay-owned enum- provider config, evaluation context, targeting keys, rules, metadata,
error_message, and full provider state are excluded
The importer first computes source_artifact_digest over the full JSONL file, then parses and reduces decision details. Receipts stay small while still binding back to the exact source artifact bytes.
The receipt is a decision-boundary artifact. It does not mean the flag decision was correct, the application behavior was safe, the provider was correct, or the targeting rules were imported as Assay truth.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
Trust Basis emits external_decision_receipt_boundary_visible when the supported OpenFeature boolean decision receipt shape is present. That claim means the bounded decision receipt boundary is visible; it does not mean the flag decision was correct, the provider was correct, the targeting rules are correct, or application behavior is safe.
Use --import-time <RFC3339> for deterministic fixture generation.
To compare the resulting Trust Basis artifact against another run, use assay trust-basis diff.
Options¶
| Option | Description |
|---|---|
--input <PATH> | OpenFeature EvaluationDetails JSONL artifact file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
Pydantic Case-Result Import¶
Import bounded Pydantic Evals reduced case-result artifacts into a verifiable Assay evidence bundle:
assay evidence import pydantic-case-result \
--input pydantic-case-results.jsonl \
--bundle-out pydantic-case-result-receipts.tar.gz \
--source-artifact-ref pydantic-case-results.jsonl
The importer is intentionally strict in v1:
- input must be JSONL with one reduced case-result artifact per row
- each row must use
pydantic-evals.report-case-result.export.v1 - each row must use
framework = pydantic_evals - each row must use
surface = evaluation_report.cases.case_result case_nameis the only docs-backed v1 case identitycase_id_refis not supported in P9dsource_case_nameandsource_refare allowed only as non-identity provenance aidsresults[]may contain bounded assertion pass/fail entries and scalar score entries only- raw
ReportCase, fullEvaluationReport, task inputs, expected outputs, model outputs, report metadata, experiment metadata, trace/span references, Logfire payloads, prompts, completions, analyses, failures, and evaluator implementation/config bodies are excluded
The importer first computes source_artifact_digest over the full JSONL file, then parses and reduces case-result artifacts. Receipts stay small while still binding back to the exact reduced source artifact bytes.
The receipt is a case-result-boundary artifact. It does not mean the evaluator judgment is correct, the model output was correct, the full ReportCase or EvaluationReport was imported, or Logfire/trace semantics are Assay truth.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
assay trust-basis generate pydantic-case-result-receipts.tar.gz --out pydantic-case-result.trust-basis.json
P9d does not add a Trust Basis claim. Pydantic case-result receipts remain importer-only until any future claim slice defines exact semantics, Trust Card impact, and Harness posture.
Use --import-time <RFC3339> for deterministic fixture generation.
Options¶
| Option | Description |
|---|---|
--input <PATH> | Pydantic Evals reduced case-result JSONL artifact file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
Promptfoo JSONL Import¶
Import Promptfoo CLI JSONL assertion component results into a verifiable Assay evidence bundle:
assay evidence import promptfoo-jsonl \
--input results.jsonl \
--bundle-out promptfoo-evidence.tar.gz \
--source-artifact-ref results.jsonl
The importer is intentionally strict in v1:
- input must be Promptfoo CLI JSONL rows
- each row must carry
gradingResult.componentResults[] - each component must be an
equalsassertion result - component scores must be binary (
0or1) - raw prompt, output, expected value, vars, and full JSONL rows are excluded
The importer first computes source_artifact_digest over the full JSONL file, then parses and reduces assertion components. That two-pass flow is intentional: receipts stay small while still binding back to the exact source artifact bytes.
result.reason is optional and bounded. For v1, failure reasons are omitted when they would leak raw compared values. Passing reasons are included only when they remain short and reviewer-safe.
The output bundle can be verified with:
The same bundle can feed the Trust Basis compiler:
This proves the imported receipts are bundleable, verifiable, and readable by the Trust Basis path. Trust Basis now emits external_eval_receipt_boundary_visible when the supported Promptfoo receipt shape is present. That claim means the bounded receipt boundary is visible; it does not mean the Promptfoo eval run passed, the model output was correct, or the raw Promptfoo payload is imported as Assay truth.
Use --import-time <RFC3339> for deterministic fixture generation.
To compare the resulting Trust Basis artifact against another run, use assay trust-basis diff.
Options¶
| Option | Description |
|---|---|
--input <PATH> | Promptfoo CLI JSONL output file |
--bundle-out <PATH> | Output Assay evidence bundle path |
--source-artifact-ref <REF> | Reviewer-safe source artifact reference stored in receipts |
--run-id <ID> | Assay import run id used for receipt provenance and event ids |
--import-time <RFC3339> | Deterministic import timestamp override |
Diff Two Bundles¶
assay evidence diff baseline.tar.gz candidate.tar.gz [--format human|json]
assay evidence diff candidate.tar.gz --baseline-dir baselines/ --key nightly
Both bundles are verified first; a bundle that fails to open or verify is an error. The comparison covers the retained verified events of both bundles and nothing outside them: absence and completeness are not established.
Two layers are compared and both are reported:
| Layer | Basis | Output |
|---|---|---|
| Retained events | verified per-event content_hash (JCS RFC 8785 + SHA-256 over the bound fields listed by assay evidence show --format json under content_hash_scope); run_root binds the ordered sequence | both run_roots; events on one side only, by content id, type and seq; run_root_equal in JSON |
| Subject projections | .net, .fs, .process subjects as sets | +/- hosts, paths, process names |
The human report (stderr) closes with exactly one of:
No differences in retained verified events: run_root equal.
Retained verified events differ: run_root differs; <n> added, <m> removed by content id.
Retained verified events differ: run_root differs; no content id added or removed (order or multiplicity differs).
A run_root that differs while no content id is added or removed means the same events in a different order or multiplicity; the line says so. Equal subject projections never make a differing run_root read as clean.
Exit code 0 means the comparison completed, with or without differences. The exit code does not encode the result; read the closing line or retained_events.run_root_equal in JSON.
See Also¶
- Evidence Contract v1
- Receipt family matrix
- Receipt schema registry
- Trust Basis CLI
- CycloneDX ML-BOM Model Component evidence example
- Mastra ScoreEvent evidence example
- OpenFeature EvaluationDetails evidence example
- Promptfoo assertion grading-result example
- From Promptfoo JSONL to Evidence Receipts
- P43 CycloneDX ML-BOM model component receipt import plan
- P14c Mastra ScoreEvent receipt import plan
- P14d Mastra score receipt Trust Basis readiness freeze
- P45b OpenFeature decision receipt Trust Basis claim plan
- P41 OpenFeature decision receipt import plan
- P31 Promptfoo receipt import plan
Sign an artifact attestation¶
The producer verifies the bounded archive and signs its complete archive digest and the artifact-derived v1.1 extent using the canonical builder. Stdout contains only the bare DSSE envelope. With --out attestation.json, the envelope is written to that file instead; the existing output-path notice goes to stderr. The subject digest binds the archive bytes, not just the semantic run root.
Verify an artifact attestation¶
assay evidence verify-attestation \
--bundle evidence.tar.gz \
--attestation attestation.json \
--key public-key.pem
The command verifies the DSSE signature with the explicitly supplied Ed25519 SPKI public PEM, then matches the complete compressed archive and every defined v1 predicate field through the canonical attestation verifier. It does not discover keys or accept a private key as a substitute. The v0 predicate and unknown predicate versions are refused.
Successful stdout is one JSON document with schema assay.evidence.attestation.verify.v1, outcome attestation_verified, signature_verified: true, subject_matched: true, artifact_sha256, predicate_type, subject_name, extent_stated and extent. The archive digest is lowercase SHA-256 hexadecimal without a prefix; it is distinct from the semantic run root.
extent is the canonical verifier's checked projection, not an unchecked copy of signed JSON. It includes retained_events_by_type (retained row counts, not hit counts) and observed.basis. Producer summaries have basis producer_reported with source_type and counts; without a recognized summary the basis is not_stated. Sandbox summaries do not state a network count, so that field remains absent rather than becoming zero. Legacy predicates with absent or null extent remain valid and report extent_stated: false and extent: null; a stated, checked extent reports extent_stated: true.
Artifact-bound support is an interpretation from the predicate specification, not a signed support_ceiling field. Neither an extent nor a valid signature establishes observation completeness. signature_verified and subject_matched remain separate statements: this command emits success only after both checks, including artifact-derived extent matching.
Exit 0 means the full attestation matched. Input, signature, integrity, version and resource refusals exit 2 with a bounded static explanation on stderr and no success JSON. A stdout write or flush failure exits 3. There is no signature-only fallback.
By default, every refusal retains exit 2 and empty stdout. The bounded static stderr explanation does not echo supplied values, but its wording is not a stable machine interface.
Use --errors-json to emit the first command refusal as one compact JSON document on stdout:
{"schema":"assay.evidence.attestation.refusal.v1","outcome":"attestation_refused","error_kind":"signature_invalid"}
The closed refusal schema defines exactly three fields and these kinds:
| Kind | Meaning |
|---|---|
malformed | Encoding, shape or parser rejection, including outer-envelope depth rejection |
unsupported | An existing type/version check refused the input; no authentication claim |
signature_invalid | The cryptographic signature check ran and failed |
limit_exceeded | A typed streaming, signed-payload, bundle or extent budget was exceeded |
bundle_rejected | Another refusal from bundle verification, not necessarily intrinsic corruption |
artifact_mismatch | Artifact digest or subject name comparison failed |
predicate_mismatch | Derived predicate or extent comparison failed |
input_unreadable | An input could not be opened or read; does not identify which input |
key_invalid | The supplied public key is not accepted as Ed25519 SPKI public PEM |
unclassified | An unclassified/future verifier error; never success |
Only the first failing check is reported; later checks may not have run. Outer-envelope parser errors remain malformed, including depth rejection, because that parser has no typed budget marker. This is not a claim that all such inputs are intrinsically malformed JSON. Input read, parse and verification order, original bytes, ceilings and acceptance are unchanged. In particular, a key-file read failure precedes envelope parsing, while invalid PEM content follows it.
The flag covers refusals inside this command after CLI argument parsing. It does not cover argument/help output, pre-dispatch failures, other commands or success-report serialization errors. Successful output remains byte-identical with or without the flag. Delivered refusal JSON still exits 2; failure to render, write or flush it exits 3, with no second JSON document or subsequent refusal diagnostic. Discard any partial output on exit 3. Static refusal stderr is best-effort: closed stderr does not change opt-in refusal exit 2, or exit 3 when stdout delivery fails. Default-mode stderr handling is unchanged.
The refusal document is not signed or authenticated and makes no claim about checks that did not run. The schema is a release/commit-bound baseline under the CLI JSON convention. Strict consumers must pair it with the emitter revision; older vendored schemas need not accept newer output. Unknown identities or kinds are unsupported, never success. Schema validation is for consumers and tests, not a new runtime input.
The signed bundle statement binds its predicate identifiers and archive/event digests under the caller-supplied key; full artifact verification also matches the supplied bundle. It does not authenticate the exact bytes of a report/profile schema or specification, or the evaluator's implementation or interpretation. Local CLI reports remain unsigned. Authenticated profile/report schema identity and digest are an unmet obligation recorded in deferred work. Implementation first requires a concrete recipient workflow that cannot be served by the existing signed artifact and pinned release/schema revision. No schema digest field or new runtime contract is introduced here.
Signatures are Ed25519 only, checked against the caller-supplied SPKI public PEM. There is no signature-algorithm field, algorithm negotiation or key wrapper. Supporting another algorithm or key format would be a separate, explicit compatibility and security decision.
Input streams are bounded before unbounded materialization or decoding: the encoded DSSE envelope is limited to 32 MiB (33,554,432 bytes), public PEM to 16 KiB (16,384 bytes), and archive source to the default 100 MiB (104,857,600 bytes). Default bundle-verification limits also apply to decoded contents, manifest, events, lines, paths and nesting. These are local refusal budgets, not a promise that every envelope below them is valid or accepted.
Outer JSON is parsed once for syntax and nesting under the standard JSON recursion limit, then that temporary value is dropped and the original bytes are parsed as a typed DSSE envelope. This small double-parse cost preserves duplicate-known-field refusal without applying the decoded Statement's string ceiling to its base64 transport. The canonical verifier independently applies strict parsing to the decoded signed Statement.
Key selection remains the caller's trust policy. Verification does not establish a trust root, transparency inclusion, observation completeness, provider outcomes or live host activity. See the v1 predicate contract.
Verify a coverage attestation¶
The command admits one local CAP-1 document under the existing byte ceiling (min(--max-bytes, 1048576)), runs the normative verifier, and writes one JSON document with schema assay.evidence.coverage_attestation.verify.v1. Stdin is not accepted. Human text is not written on stdout. A refusal prints one value-free line on stderr from the verifier's Display.
Stdout outcome is cap1_conforms, cap1_refused, or verification_unavailable. Exit 0 is conformance; exit 2 is a contract refusal (E_EVIDENCE_CONTRACT) or an unreadable input (E_EVIDENCE_UNREADABLE). A stdout write or flush failure exits 3.
input_sha256 is the SHA-256 of the complete raw bytes, or null when those bytes were not completely read. schema_pin names the embedded CAP-1 schema and its digest. document carries profile, stratum count, integrity.complete as integrity_complete, and the absence-assertion count; it is null when the document did not conform. claim_gate is always not_evaluated.
Conformance is internal consistency of one document, not producer truth, not capture completeness. The claim gate did not run. verification_unavailable is not a refusal. A conforming document is not a clean result.
--max-bytes selects a budget and never raises the hard maximum. Nobody signs this document; it is a fresh local result.
Export and verify an incident package¶
On Unix, prepare the canonical inventory.json and recorded assessment.json defined by the Incident Package v1 contract. Supply one regular file for each unique SHA-256 digest named in the inventory. The exporter packages those exact bytes; it does not discover a session or generate the assessment. For example, with one object:
umask 077
assay evidence export-incident-package \
--inventory inventory.json --assessment assessment.json \
--object 'SHA256_OF_SOURCE=source.json' \
--out incident.tar > export-report.json
assay evidence verify-incident-package incident.tar > fresh-verify.json
Replace SHA256_OF_SOURCE with the object's actual 64-character lowercase hex digest, and repeat --object for each distinct digest. If the assessment was made with an external context, pass the same --context context.json to both commands. The exporter checks the recorded assessment against fresh verification, then publishes a new archive without replacing an existing destination. Its JCS-plus-newline stdout report contains artifact_sha256; the redirected report stays outside the archive. The verifier recomputes a fresh report from the archive and selected context. Check each command's exit status and outcome, and reverify the archive when using it later; a saved report is not a reusable OK authority. Exit 0 means package_verified, exit 2 is refusal or unavailable input, and stdout failure exits 3. The Unix exporter publishes with mode at most 0600, subject to umask; the shell creates redirected report files under the caller's umask.
Select and review original bytes before export. They may contain private paths, hosts, commands, transcripts or credentials. Redaction changes the bytes and digest and creates a distinct derivative; record what can no longer be reverified when originals are withheld. Context statements about custody or retention are operator assertions unless separately supported, and a retention statement is not a legal hold. Do not put secrets in diagnostics or automatically collect principal credentials. Verification accounts for retained supplied records under the selected context; it does not prove capture completeness, provider outcomes, session activity absence, or a cold-machine run.
For a retained five-surface session and the minimal offline delivery recipe, see the incident replay fixture. The existing CLI is the receiving verifier; that recipe does not establish parity with a separate leaf verifier.
Read the verified package¶
After package_verified, read the supporting documents from the same unchanged archive:
These commands print members to stdout without extracting files to disk. In inventory.json, inputs describes the supplied sources and context records available and missing context. In assessment.json, joins and claims describe the assessed links and claims, and surfaces contains the observation grades. The package contract defines these fields.
When cap1 is non-null, read cap1.strata[*].supports together with cap1.strata[*].population: the population is retained supplied records. These supports values, including bounded_negative and exhaustive_set, apply only to that enumerated population. They do not establish complete capture of real activity or turn unknown observation coverage into known coverage.
Handle a refusal¶
Keep the command's exit status, JSON report and stderr. input_shape is a broad refusal category, not a diagnosis of the precise defect. A matching transport checksum does not establish package validity. Comparing or hashing members yourself is optional troubleshooting; it does not make parts of a refused package verified evidence.
Evidence Store Commands (BYOS)¶
Assay supports Bring Your Own Storage (BYOS) over S3, Azure Blob, Google Cloud Storage, and local filesystems.
Store Status¶
Check store connectivity, read/write permissions, and Object Lock status:
object_lock reports an observed status from a closed vocabulary:
| Status | Meaning |
|---|---|
enabled | Object Lock is enabled on the bucket |
disabled | Object Lock is disabled on the bucket |
unobserved:unsupported_backend | Storage backend does not support Object Lock (e.g. file://, memory://) |
unobserved:not_probed | Backend supports Object Lock (e.g. S3), but live probing has not been run |
unobserved:permission_denied | Credentials lack permission to query Object Lock configuration |
unobserved:error | Probe error encountered while inspecting Object Lock configuration |
Push¶
Upload an evidence bundle to the store:
Pull¶
Download an evidence bundle from the store:
assay evidence pull --bundle-id <ID> [--store <URL>] [-o <PATH>] [--verify]
assay evidence pull --run-id <RUN_ID> [--store <URL>] [-o <DIR>] [--verify]
With --verify, the downloaded bundle is verified before anything is written to disk, binding the served archive to the requested bundle_id.
List¶
List evidence bundles in the store:
assay evidence list [--store <URL>] [--run-id <RUN_ID>] [--prefix <PREFIX>] [--limit <N>] [--format plain|json|table]
Index Rebuild¶
Reconstruct the non-canonical run index (runs/{run_id}/{bundle_id}.ref) from canonical bundles (bundles/{bundle_id}.tar.gz):
- Discovers canonical bundles from
bundles/. - Verifies each bundle's full cryptographic integrity and manifest binding (
bundle_id == run_root). - Skips and reports corrupted bundles without linking them.
- (Re)creates missing run links under
runs/{run_id}/{bundle_id}.ref. - Idempotent: existing links are preserved and repeated runs link 0 new refs.
- Detects and reports stale references (refs pointing to bundles missing from the canonical store) without deleting or trusting them.
- In JSON mode, emits document schema
assay.evidence.index_rebuild.v0.
Exit Codes¶
| Exit Code | Condition | Behavior |
|---|---|---|
0 | Success | All discovered bundles verified and indexed successfully. If stale references are detected, they are listed and reported, but the command exits 0. |
1 | Verification failure / Store error | One or more canonical bundles failed cryptographic or schema verification, or a store I/O error occurred. |
2 | Configuration error | Store URL resolution failed or the store specification URL is invalid. |