Skip to content

Artifact format

This document describes how Aval stores an execution result in an OCI registry, the attestation it signs, and the JSON that aval verify --output json prints. It is the contract external tools consume through pkg/evidence.

Status: pre-alpha. The Go types in pkg/evidence are the source of truth. Signing, push, lookup, fetch, verification and the policy file are implemented (attest.Sign, registry.Push, registry.Lookup, registry.Fetch, evidence.Verify, policy.Load). aval verify, aval run and aval push use them.

The identifiers below and the JSON shape of the statement, predicate and verify report are frozen. Within a version they change additively only; see Versioning and compatibility.

Identifiers

All constants live in pkg/evidence.

Constant Value Used for Status
ArtifactType application/vnd.trianalab.aval.execution.v2 artifactType of the OCI manifest frozen
ArtifactTypeFingerprint application/vnd.trianalab.aval.fingerprint.v1 artifactType of the per-fingerprint anchor manifest frozen
MediaTypeDSSE application/vnd.dsse.envelope.v1+json Media type of the signed DSSE envelope layer DSSE standard
MediaTypeExecution application/vnd.trianalab.aval.execution.v2+json Media type of the raw predicate JSON, when carried on its own frozen
PayloadType application/vnd.in-toto+json DSSE payloadType in-toto standard
StatementType https://in-toto.io/Statement/v1 in-toto Statement _type in-toto standard
PredicateType https://aval.trianalab.io/attestation/execution/v2 in-toto predicateType frozen
SchemaVersion v2 Predicate schemaVersion frozen
ReportVersion aval.trianalab.io/verify-report/v1 version of the verify report frozen
RunRecordVersion aval.trianalab.io/run-record/v1 version of a run record provisional
ArtifactTypeRun application/vnd.trianalab.aval.run.v1 artifactType of a run record manifest provisional
MediaTypeRun application/vnd.trianalab.aval.run.v1+json Media type of the run record layer provisional
ArtifactTypeStats application/vnd.trianalab.aval.stats.v1 artifactType of the per-test stats anchor manifest provisional

Registry identifiers (internal/registry): the bundle layer uses the Sigstore media type application/vnd.dev.sigstore.bundle.v0.3+json (MediaTypeSigstoreBundle), and manifests carry the annotation io.trianalab.aval.fingerprint (AnnotationFingerprint, informational: verification reads the fingerprint from the signed predicate). The annotation key is frozen. The stats anchor carries io.trianalab.aval.test (AnnotationTest, provisional) with the test id.

Related identifiers from other packages: the fingerprint manifest format is aval-fingerprint/v1 (internal/fingerprint), and the contract and policy apiVersion is aval.trianalab.io/v1 (internal/contract, see contract.md). Both are frozen. The fingerprint manifest version is a format tag, not a namespaced identifier: it names the canonical manifest layout and hash, not a resource, so it stays outside aval.trianalab.io and is frozen as aval-fingerprint/v1; a breaking change mints aval-fingerprint/v2.

OCI layout

There is no Aval server or database. The registry is the only index.

Per fingerprint there is one anchor manifest, and one execution manifest per execution whose subject is the anchor, so the OCI 1.1 referrers API lists every execution for a fingerprint.

The anchor is deterministic: its bytes, and so its digest, depend only on the fingerprint. It is tagged sha256-<fingerprint hex> (registry.Tag):

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.trianalab.aval.fingerprint.v1",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2,
    "data": "e30="
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.empty.v1+json",
      "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
      "size": 2,
      "data": "e30="
    }
  ],
  "annotations": {
    "io.trianalab.aval.fingerprint": "sha256:<fingerprint hex>"
  }
}

One execution = one OCI 1.1 image manifest, following the OCI 1.1 artifact guidance (artifactType set, empty config):

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.trianalab.aval.execution.v2",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2,
    "data": "e30="
  },
  "layers": [
    {
      "mediaType": "application/vnd.dsse.envelope.v1+json",
      "digest": "sha256:<envelope digest>",
      "size": 1834
    },
    {
      "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "digest": "sha256:<bundle digest>",
      "size": 4210
    }
  ],
  "subject": {
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "digest": "sha256:<anchor digest>",
    "size": 512
  },
  "annotations": {
    "io.trianalab.aval.fingerprint": "sha256:<fingerprint hex>"
  }
}
  • config: the OCI empty descriptor ({}, media type application/vnd.oci.empty.v1+json).
  • layers: the signed DSSE envelope (see below), then the Sigstore bundle with the verification material. registry.Push refuses an attestation whose bundle is missing or is not valid JSON. If registry.Fetch reads a manifest without a bundle layer, the attestation can never verify, so it ends as a verification failure. registry.Fetch rejects any other layer, a repeated layer, or a missing envelope as a verification failure, and so a manifest that is not sha256-addressed, is larger than 4 MiB, or does not match the digest it was fetched by.
  • subject: always the anchor of the execution's fingerprint. The tested revision or artifact is recorded inside the statement (subject), not in the manifest, so the referrers of a tested image do not list its evidence.

The execution.v2 identifiers replace execution.v1, whose manifest had the tested artifact as an optional subject and exactly one layer. Aval no longer reads or writes v1: there is no accept window, and a v1 artifact is never a candidate.

Push

registry.Push uploads the blobs, pushes the anchor and (re)tags it, then pushes the execution manifest by digest. It never moves or replaces an earlier execution: pushing the same bytes again is a no-op, and anything else becomes one more referrer of the anchor. Re-tagging the deterministic anchor also repairs a tag that was moved.

Registries without the referrers API (for example GHCR) are supported: oras maintains the OCI referrers tag schema (sha256-<anchor hex> index) on push and reads it on lookup. That index is rewritten on every push, so two concurrent pushes for the same fingerprint can drop one entry from it. Push therefore lists the anchor's referrers after pushing and returns only once the new digest is among them; if it is not, it pushes the manifest again (which re-adds it to the index) and lists again, three attempts in total, one second apart. If the digest is still not listed, aval push and aval run --push fail (exit 1). A registry that indexes referrers asynchronously for longer than about two seconds also fails the push; rerun it. Once a fingerprint has more than 64 attestations the confirmation listing fails too, so push exits 1. A dropped execution is never a candidate, so even then it never makes an unsigned or untrusted result reusable. Aval does not delete the previous referrers index after rewriting it, so pushing does not need DELETE rights.

Lookup and consumption

  1. Compute the fingerprint locally (aval run --fingerprint-only), e.g. sha256:3f9a....
  2. Resolve the tag sha256-<fingerprint hex> in the repository passed via --registry. It must point at the anchor digest Aval computes locally for that fingerprint; anything else is a verification failure (the tag was moved).
  3. List the anchor's referrers with artifactType application/vnd.trianalab.aval.execution.v2. Each one is a candidate, identified by its manifest digest, even when the referrer entry is malformed (not an image manifest, not sha256): Fetch then rejects it as a verification failure, so it never turns into a miss.
  4. Fetch each candidate by digest (repo@sha256:...). Never fetch by tag: tags are mutable, digests aren't.
  5. Verify each envelope and apply the trust policy (see below).

Lookup accepts at most 64 candidates per fingerprint and makes at most 64 requests to list referrers, whatever paths the registry's Link headers point at. Above either limit it fails with outcome error (run the test), so whoever can push, or a buggy registry, cannot make aval verify fetch an unbounded number of manifests or pages.

A listed candidate whose manifest the registry answers with 404 (deleted, for example by an "untagged manifests" retention policy) is absent: it is skipped with a warning on stderr and is not part of rejected. If every candidate is absent, the outcome is a miss. Absence never hides a verification failure of another candidate.

Every registry call of one command runs under --registry-timeout (default 1m): lookup and every fetch share one deadline, and push has its own. A registry that stalls past it is outcome error (exit 1), so aval run runs the test; runner.command is never under this deadline. One credential store and one auth token cache are shared by all registry calls of a process.

A missing tag, or an anchor without executions, is a miss (evidence.ErrNotFound, exit code 4). An artifact that exists but is malformed or fails signature or policy checks is a verification failure (evidence.ErrVerification, exit code 5) and is a security event.

Registry credentials

Aval reads registry credentials the way Docker does: from $DOCKER_CONFIG/config.json, or ~/.docker/config.json when DOCKER_CONFIG is not set, including the credential helpers it names (credsStore, credHelpers, for example docker-credential-osxkeychain or docker-credential-ecr-login). Log in with docker login <registry> (or oras login, which writes the same file). Without an entry for the registry, Aval connects anonymously, which is enough to read a public repository. Every registry call runs under --registry-timeout (default 1m; see Lookup and consumption).

A config file that cannot be parsed, or a helper that fails, is outcome error (exit 1): aval run runs the test and nothing is reused. Loopback registries (localhost, 127.0.0.1, ::1) use plain HTTP; every other registry uses HTTPS.

Multiple attestations per fingerprint

Several executions can produce the same fingerprint (a developer run and a CI run, or a re-run). All of them stay listed as referrers of the anchor, so a later push cannot hide or replace an earlier one. aval verify verifies every candidate: the verified one with the highest trust level wins (ties go to a non-passed result, then the lowest digest). Candidates that are not used are listed in the report's rejected field; when any of them failed verification, stderr gets one aval: SECURITY: ... line. A candidate that cannot be fetched or checked (outcome error) might have outranked the verified one, so it blocks reuse and the test runs. A candidate that passes every check but whose signer the policy lists below minTrustLevel is rejected as miss: it was fully evaluated, so it does not block a verified sibling. When nothing is reused, the outcome is verification-failed when any candidate failed verification, otherwise error when any could not be checked, otherwise miss.

The 64-candidate cap and cleanup

Every push adds one execution, even for the same inputs: the hostname and timestamps in the predicate differ. Aval never deletes one. Once a fingerprint has more than 64 executions, every lookup for it is outcome error: the test always runs. Every push for it still adds an execution, then fails its confirmation and exits 1. It stays that way until someone deletes executions. Other fingerprints are not affected, so the next change to an input also ends it.

To stay below the cap, push from one place (for example CI on the main branch). aval run --push does not push after it reuses a result, but aval run --shadow --push runs and pushes every time.

To clean up a fingerprint, list its executions and delete the ones you no longer need, for example with ORAS:

aval run --fingerprint-only            # prints sha256:<hex>
oras discover --artifact-type application/vnd.trianalab.aval.execution.v2 <repo>:sha256-<hex>
oras manifest delete <repo>@sha256:<execution digest>

The registry must allow deletes (registry:2 needs REGISTRY_STORAGE_DELETE_ENABLED=true; on GHCR, delete the package versions). Deleting executions is safe: it can make a result stop being reusable, never make an untrusted one reusable. Keep the one you still want to reuse, and keep any execution that failed verification until you have looked into it.

Run records

aval run --record publishes one run record per run, for every outcome (verified, miss, error, verification-failed), so reuse can be measured from the registry alone. --record-file <path> writes the same record to a file, and aval record --record-file <path> publishes it later, for example from a CI job that has write credentials but runs no tests.

Records are statistics, never evidence. They are unsigned and informational. aval run and aval verify never fetch or read them, so a record, forged or not, cannot cause reuse, change an outcome or count toward the 64-candidate cap.

Recording is best-effort: a record that cannot be published (no write credentials, registry down, past --registry-timeout) or written prints aval: warning: run record not ... on stderr and never changes the exit code. Publishing has its own --registry-timeout deadline, so it cannot hold the command longer. --record needs --registry. Aval keeps no local state; a record file exists only when --record-file asks for one. An existing file at --record-file that is not an Aval result, report or valid run record is never overwritten: the run warns and goes on, and inside the contract scope that file stays an input of the fingerprint. A --record-file that is also --result or --report is not written either.

Layout

Per test there is one stats anchor, separate from every fingerprint anchor. It is deterministic (its digest depends on the test id alone) and tagged stats-<test id> (registry.StatsTag); fingerprint tags and OCI referrers tag schema tags start with sha256-, so the tags cannot collide. It has the same shape as the fingerprint anchor, with "artifactType": "application/vnd.trianalab.aval.stats.v1" and the annotation "io.trianalab.aval.test": "<test id>".

Each record is an OCI 1.1 image manifest with "artifactType": "application/vnd.trianalab.aval.run.v1", the empty config, one layer of media type application/vnd.trianalab.aval.run.v1+json holding the record JSON, and the stats anchor as subject. Records are listed by the stats anchor's referrers. Unlike registry.Push, publishing a record does not confirm the referrers listing, so on a registry without the referrers API two concurrent records for one test can drop one of them. On such registries all records of a test also share one referrers index, which grows with every record; once it exceeds the registry's manifest size limit, publishing a record fails with a warning. Pruning old records is future work (Epic 9, #88).

Record JSON

evidence.RunRecord, parsed by evidence.ParseRunRecord:

Field Type Present Meaning
version string always aval.trianalab.io/run-record/v1.
testId string always metadata.name from the contract.
fingerprint string always sha256:<hex>; empty when the fingerprint could not be computed.
outcome string always The lookup decision, as in the verify report.
reused bool always true when a verified result was reused and the test did not run. With --shadow, or when --report cannot be written, it is false.
result string when known The reused or the real passed / failed; omitted when runner.command could not start.
testDurationMs integer always How long runner.command ran, in milliseconds; 0 when it did not run.
savedDurationMs integer always On reuse, the test duration recorded in the reused attestation (finishedAt - startedAt); 0 otherwise.
avalVersion string always Version of the aval binary that ran.
recordedAt RFC 3339 timestamp always When the record was made.
ciRunUrl string when passed --ci-run-url, a link to the CI run. Aval does not derive it from any CI vendor.

Example:

{
  "version": "aval.trianalab.io/run-record/v1",
  "testId": "orders-integration",
  "fingerprint": "sha256:3f9a7c2e1b4d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7",
  "outcome": "verified",
  "reused": true,
  "result": "passed",
  "testDurationMs": 0,
  "savedDurationMs": 1184000,
  "avalVersion": "0.3.0",
  "recordedAt": "2026-11-06T09:00:00Z",
  "ciRunUrl": "https://ci.example.com/acme/orders/runs/42"
}

DSSE envelope

The layer is a DSSE envelope (evidence.Envelope):

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<base64 of the in-toto Statement JSON>",
  "signatures": [
    { "keyid": "", "sig": "<base64 signature>" }
  ]
}

Signing uses sigstore-go (attest.Sign, keyless with a Fulcio certificate and Rekor entry, or key-based; see trust-model.md). It also produces a Sigstore bundle (application/vnd.dev.sigstore.bundle.v0.3+json) carrying the certificate or key hint and the Rekor entry; it is stored as the second layer of the execution manifest. registry.Push requires it: aval verify checks the signature through the bundle (attest.Verifier), so an attestation without one could only ever be a verification failure.

in-toto Statement

The envelope payload is an in-toto Statement v1 (evidence.Statement). Its subject is the tested revision or artifact, the same one recorded in predicate.subject.

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "git+https://github.com/acme/orders",
      "digest": { "gitCommit": "9b2c1e0f4a7d3b5c6e8f9a0b1c2d3e4f5a6b7c8d" }
    }
  ],
  "predicateType": "https://aval.trianalab.io/attestation/execution/v2",
  "predicate": { "...": "see below" }
}

For an image, use "digest": {"sha256": "<hex>"} with the image reference as name.

aval run records name = the test id and digest = {"sha256": "<fingerprint hex>"} (the declared inputs), runner.identity = the host name (a claim; trust comes from the signer), runner.trustLevel = 0, and execution.environment = os and arch plus the declared toolchain. Recording a git commit or image as the subject is milestone 3 work.

Predicate

evidence.Predicate, JSON tags exactly as below. Required fields are the ones checked by Predicate.Validate (called by evidence.Parse).

Field Type Required Meaning
schemaVersion string yes, must be v2 Predicate schema version.
subject object yes Tested revision/artifact.
subject.name string yes e.g. git+https://github.com/acme/orders or an image reference.
subject.digest map string to string yes, non-empty e.g. {"gitCommit": "..."} or {"sha256": "..."}.
test object Test identity: what was checked, independent of inputs.
test.id string yes metadata.name from aval.yaml.
test.cacheability string Class declared in the contract: hermetic, controlled-environment, external-dependencies, non-deterministic.
result string yes passed or failed.
execution object This run: its inputs and conditions.
execution.fingerprint string yes, sha256:<64 hex> Fingerprint over the declared inputs.
execution.fingerprintVersion string Manifest format, e.g. aval-fingerprint/v1.
execution.environment map string to string no (omitted when empty) Conditions of the run (toolchain, OS/arch). Only keys declared under environment.toolchain in aval.yaml are part of the fingerprint; anything else here is informational.
execution.exitCode integer Exit code of runner.command.
runner object Who produced the result, as claimed.
runner.identity string yes Claimed producer identity.
runner.issuer string no (omitted when empty) Claimed identity issuer, e.g. an OIDC issuer URL.
runner.trustLevel integer 0 unknown, 1 developer, 2 runner, 3 isolated.
startedAt RFC 3339 timestamp yes Execution start.
finishedAt RFC 3339 timestamp yes, not before startedAt Execution end.
annotations map string to string no (omitted when empty) Free third-party metadata. Aval never reads it.

runner.identity, runner.issuer and runner.trustLevel are claims written by whoever produced the predicate. The authoritative identity and trust level are those of the verified signer, matched against the policy's accepted identities (evidence.Verification.Signer and its TrustLevel). Consumers must not make trust decisions from the runner block. The Go fields Runner.Issuer, Runner.TrustLevel and Verification.TrustLevel are deprecated: Aval leaves the first two empty, and the third duplicates Verification.Signer.TrustLevel.

evidence.Parse only checks structure. A parsed predicate is a claim, not evidence; use evidence.Verify to get something you can rely on.

Annotations

Keys use reverse-DNS of the owner, e.g. com.example.team, com.acme.ticket. Aval does not define, validate or interpret any key. Do not put secrets there: the predicate is published to the registry.

Example

{
  "schemaVersion": "v2",
  "subject": {
    "name": "git+https://github.com/acme/orders",
    "digest": { "gitCommit": "9b2c1e0f4a7d3b5c6e8f9a0b1c2d3e4f5a6b7c8d" }
  },
  "test": {
    "id": "orders-integration",
    "cacheability": "controlled-environment"
  },
  "result": "passed",
  "execution": {
    "fingerprint": "sha256:3f9a7c2e1b4d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7",
    "fingerprintVersion": "aval-fingerprint/v1",
    "environment": {
      "go": "1.23.4",
      "os": "linux",
      "arch": "amd64"
    },
    "exitCode": 0
  },
  "runner": {
    "identity": "https://github.com/acme/orders/.github/workflows/ci.yaml@refs/heads/main",
    "issuer": "https://token.actions.githubusercontent.com",
    "trustLevel": 2
  },
  "startedAt": "2026-11-05T10:12:03Z",
  "finishedAt": "2026-11-05T10:31:47Z",
  "annotations": {
    "com.acme.team": "payments"
  }
}

Verify report

aval verify --output json prints one evidence.Report to stdout, and aval run --report <file> writes the same document for its lookup decision. This is the stable machine interface for CI scripts and external tools.

Field Type Present Meaning
version string always aval.trianalab.io/verify-report/v1.
outcome string always See outcomes below.
testId string always metadata.name from the contract.
fingerprint string always Locally computed sha256:<hex>.
reference string when an attestation was found Immutable repo@sha256:<manifest digest>.
result string verified only passed or failed from the verified predicate.
signer object verified only Accepted policy identity that matched (issuer, subjectRegexp, trustLevel) plus the identity the signature actually carried: subject, the certificate subject (SAN) for keyless signatures, or keyId, base64(sha256(DER public key)) for key-based ones. subject and keyId are omitted when empty.
predicate object verified only Full verified predicate.
reason string non-verified outcomes Human-readable cause.
rejected array when a candidate was not used Each {reference, outcome, reason}; outcome is verification-failed, error or miss. A verification-failed entry is a security event even when outcome is verified: alert. A miss entry is an authentic attestation whose signer the policy lists below minTrustLevel: routine non-reuse, no alert.

Outcomes and exit codes:

outcome Meaning What the caller does Exit code
verified Valid attestation satisfies the policy. Reuse result. 0
miss No attestation for this fingerprint that the policy can reuse: none found, or only authentic ones whose signer the policy lists below minTrustLevel. Run the test. 4
verification-failed Attestation exists but fails signature or policy. Run the test and alert. Never hide this. 5
error Aval could not decide (registry unreachable, bad policy file, trusted root unavailable). Treat as a miss: run the test (fail closed). 1

These are the aval verify exit codes. aval run acts on the outcome and exits with the result instead; see trust-model.md.

If the fingerprint cannot be computed (empty scope, missing dependency, symlink, control characters in a path, I/O error), aval verify prints no report and exits 1; bad flags or an invalid contract print no report and exit 2. aval run --report still writes one in that case, with outcome error, the reason, and an empty fingerprint, and then runs the test.

Without --policy, aval verify skips the lookup and reports outcome: "miss" (exit 4), and so it does for a non-deterministic contract. With --policy, a policy file that cannot be loaded is outcome: "error" (exit 1) and no lookup is made. A trusted root that cannot be loaded is outcome: "error" (exit 1) once a candidate with a certificate needs it.

Example (verified):

{
  "version": "aval.trianalab.io/verify-report/v1",
  "outcome": "verified",
  "testId": "orders-integration",
  "fingerprint": "sha256:3f9a7c2e1b4d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7",
  "reference": "ghcr.io/acme/orders-aval@sha256:8c1d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a8b9c0",
  "result": "passed",
  "signer": {
    "issuer": "https://token.actions.githubusercontent.com",
    "subjectRegexp": "^https://github.com/acme/orders/\\.github/workflows/ci\\.yaml@refs/heads/main$",
    "trustLevel": 2,
    "subject": "https://github.com/acme/orders/.github/workflows/ci.yaml@refs/heads/main"
  },
  "predicate": { "...": "as in the predicate example" }
}

Example (miss):

{
  "version": "aval.trianalab.io/verify-report/v1",
  "outcome": "miss",
  "testId": "orders-integration",
  "fingerprint": "sha256:3f9a7c2e1b4d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7",
  "reason": "no attestation found for fingerprint"
}

Versioning and compatibility

These rules apply to every frozen format:

Format Version identifiers (move together) Frozen at
Statement and predicate PredicateType, SchemaVersion, MediaTypeExecution, ArtifactType .../execution/v2, v2, execution.v2
Fingerprint anchor ArtifactTypeFingerprint fingerprint.v1
Verify report, outcomes and exit codes ReportVersion aval.trianalab.io/verify-report/v1
Run record and stats anchor RunRecordVersion, ArtifactTypeRun, MediaTypeRun, ArtifactTypeStats run-record/v1, run.v1, stats.v1 (provisional)
Contract and policy file apiVersion aval.trianalab.io/v1
Fingerprint manifest header line, execution.fingerprintVersion aval-fingerprint/v1

The predicate keeps v2: v1 was used before and an identifier is never recycled, and v2 is already under aval.trianalab.io and is what v0.1.0 writes, so attestations published by v0.1.0 stay readable. The report and the contract moved from the pre-freeze aval.dev/.../v1alpha1 names to aval.trianalab.io at v1. There is no accept window for the old names: a contract or policy file with aval.dev/v1alpha1 is invalid.

Additive changes (same identifier)

  • A new optional field, omitted when empty, whose absence means what it meant before.
  • A new key under annotations.
  • A new optional contract field. Older aval binaries reject a contract that uses it (unknown fields are rejected), which fails closed.

Existing fields keep their name, JSON tag, type and meaning.

Breaking changes (new identifier)

  • Renaming or removing a field, changing its type or its meaning.
  • Making an optional field required, or tightening validation of an existing one.
  • A new value of a closed set: outcome, result, trustLevel, cacheability class, or an exit code.
  • A change to what the fingerprint covers or how it is hashed: a new aval-fingerprint/vN. There is no accept window: old attestations become misses, never false hits.

A new predicate version changes PredicateType, SchemaVersion, MediaTypeExecution and ArtifactType together (execution/v3, execution.v3). Old and new artifacts can coexist in one repository.

How consumers treat versions

  • Unknown version: not evidence. Reject a predicateType, schemaVersion, report version or artifactType you do not know, and run the test. evidence.Verify does this for predicates.
  • Unknown fields: ignore them within a version you know. Do not decode Aval documents strictly. evidence.Parse ignores unknown fields.
  • Unknown values of a closed set: fail closed. An outcome other than the four listed above, a result other than passed or failed, or an unknown trust level or cacheability class means "not verified": run the test. Within a version such a value can only come from a bug or tampering.
  • No field grants trust. Trust comes from the verified signer and the policy (see trust-model.md), never from a field that is new or that you do not understand.

Deprecation

Aval writes only the newest version of each format. When a breaking change mints a new version, the release notes say whether Aval keeps reading the old one for a transition. If it does, the old parser stays next to the new one, dispatching on the identifier, and old documents get no more trust than new ones; any identifier Aval does not know fails closed. Ending a transition is announced in the release notes of the release that removes it.

Golden files

pkg/evidence/testdata/statement-execution-v2.golden.json, pkg/evidence/testdata/verify-report-v1.golden.json and pkg/evidence/testdata/run-record-v1.golden.json set every field of the statement, predicate, verify report and run record. A test decodes each strictly into the current types and requires the same JSON back, so dropping, renaming or retyping a field, or adding a required one, fails. Another test requires every JSON field of those types to be set in its golden file, so a new field fails until it is added there. Golden files only grow: an existing field or value is never removed or changed, an additive change adds its new field to the golden file of its version, and a new version adds a new file next to the old ones.

History

  • Predicate v1 became v2 when the execution manifest's subject changed from the tested artifact to the fingerprint anchor and the optional Sigstore bundle layer was added.
  • At the freeze (#17), the verify report moved from aval.dev/verify-report/v1alpha1 to aval.trianalab.io/verify-report/v1 and the contract and policy apiVersion from aval.dev/v1alpha1 to aval.trianalab.io/v1, without other changes. The contract hash covers apiVersion, so every fingerprint changed: results published before are misses and the tests run once.
  • Within verify-report/v1 (#65), a signer the policy lists below minTrustLevel became miss instead of verification-failed, and rejected[].outcome may now also be miss. No new value, field or exit code; the maintainers confirmed this as additive, so the version stays.