Skip to content

assay evidence

Manage Assay evidence bundles and external evidence imports.


Synopsis

assay evidence <COMMAND> [OPTIONS]

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 as audit_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 in audit_records_ambiguous rather than in audit_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_reason
  • bounded_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-ref and name
  • if multiple model components exist, --bom-ref is required
  • full BOM graphs, modelCard bodies, 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:

assay evidence verify cyclonedx-model-receipt.tar.gz

The same bundle can feed the Trust Basis compiler:

assay trust-basis generate cyclonedx-model-receipt.tar.gz --out cyclonedx-model.trust-basis.json

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
  • score must be numeric
  • target_ref and at least one scorer identity (scorer_id or scorer_name) must be present
  • score_id_ref maps to Mastra scoreId when present; it is live-backed on @mastra/core 1.29.1 / @mastra/observability 1.10.2, but remains optional in the v1 reduced artifact for older captures and compatibility fixtures
  • score_source, trace_id_ref, span_id_ref, and score_trace_id_ref are reviewer aids only, not receipt identity in v1
  • raw exporter callback payloads, raw metadata, raw correlationContext, 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:

assay evidence verify mastra-score-receipts.tar.gz

The same bundle can feed the Trust Basis compiler:

assay trust-basis generate mastra-score-receipts.tar.gz --out mastra-score.trust-basis.json

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 type must be function_tools_executed when 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_id presence is accepted and still uses list-order pairing
  • missing FunctionCallOutput / null output entries are preserved as completed=false without inferring is_error
  • raw tool arguments and outputs are accepted only as fixture input for hashing; receipts store arguments_hash / output_hash or 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:

assay evidence verify livekit-tool-action-receipts.tar.gz

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 EvaluationDetails artifact per row
  • each row must use openfeature.evaluation-details.export.v1
  • each row must represent target_kind = feature_flag
  • result.value must be boolean
  • result.reason is 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:

assay evidence verify openfeature-decision-receipts.tar.gz

The same bundle can feed the Trust Basis compiler:

assay trust-basis generate openfeature-decision-receipts.tar.gz --out openfeature.trust-basis.json

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_name is the only docs-backed v1 case identity
  • case_id_ref is not supported in P9d
  • source_case_name and source_ref are allowed only as non-identity provenance aids
  • results[] may contain bounded assertion pass/fail entries and scalar score entries only
  • raw ReportCase, full EvaluationReport, 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:

assay evidence verify pydantic-case-result-receipts.tar.gz

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 equals assertion result
  • component scores must be binary (0 or 1)
  • 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:

assay evidence verify promptfoo-evidence.tar.gz

The same bundle can feed the Trust Basis compiler:

assay trust-basis generate promptfoo-evidence.tar.gz --out promptfoo.trust-basis.json

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

Sign an artifact attestation

assay evidence attest --bundle evidence.tar.gz --key private-key.pem

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

assay evidence verify-coverage-attestation --document coverage.json

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:

tar -xOf incident.tar inventory.json
tar -xOf incident.tar assessment.json

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:

assay evidence store-status [--store <URL>] [--store-config <PATH>] [--format table|json|plain]

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:

assay evidence push <BUNDLE> [--store <URL>] [--run-id <RUN_ID>] [--allow-exists]

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):

assay evidence index rebuild [--store <URL>] [--store-config <PATH>] [--format summary|json]
  • 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.