context-report attestation v0.1

This is the normative specification for the context-report predicate. The JSON Schema at schema.json is authoritative; this document explains what its fields mean and what a producer or verifier MUST, SHOULD, or MAY do with them. Where the two disagree, the schema wins and this file has a bug.

The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" are to be interpreted as in RFC 2119, following the convention of the SLSA and in-toto specifications this predicate borrows from.

Purpose

A context-report statement records measured facts about one agent context artifact — a plugin, an AGENTS.md or other instruction file, a skill, a hook, an MCP server, or a subagent definition — as observed by one target agent. Two harnesses can disagree about the same artifact: a hook that fails closed on a crash under one client and fails open under another is not a detail, it is the security-relevant fact the report exists to carry. A statement therefore covers exactly one (subject digest, target) pair, and its rows are facts, never a verdict: the predicate MUST NOT assert that the artifact as a whole "passes" or "fails". A consumer (a plugin catalog, a registry, a reviewer) sets its own thresholds over the rows it cares about; the format's job ends at stating the fact accurately and saying how it was obtained.

Envelope

A context-report statement is an in-toto Statement/v1 whose predicateType is https://open-coder-ai.github.io/context-report/attestation/v0.1 and whose predicate conforms to schema.json. Producers MUST emit exactly one statement per (subject digest, target) pair — an artifact attested for four agents is four statements against the one subject digest, never a single statement with four targets.

Per in-toto Statement/v1, subjects are matched purely by digest: subject[] entries carry a digest (this predicate's schema requires it via resourceDescriptor), and a name or uri alone never binds a statement to an artifact. Consumers MUST ignore fields they do not recognize — this is what lets attributes[].values, .conditions, and .environment stay producer-defined free-form objects without breaking older verifiers.

Signing. A producer SHOULD emit the statement via actions/attest, which wraps it in a DSSE envelope inside a Sigstore bundle, signs it with the CI workflow's OIDC identity via Fulcio, and records it in the public Rekor log. This is why the format asks nothing of the author's laptop: the trust a consumer places in a row comes from predicate.producer.id (see below) carrying the workflow's identity, not a claim typed by a human.

Verification. A consumer verifies with gh attestation verify <artifact> --predicate-type https://open-coder-ai.github.io/context-report/attestation/v0.1, optionally narrowed with --signer-workflow <owner>/<repo>/<path>@<ref> and --deny-self-hosted-runners to require "the official action, on a hosted runner" rather than merely "some signature exists". A verifier MUST reject a statement whose predicateType does not match exactly — 0.X is part of the identity (see Versioning).

Predicate fields

predicate is required to carry subjectKind, target, producer, and attributes; metadata, configuration, resolvedDependencies, and byproducts are optional.

A resourceDescriptor (in-toto Statement/v1, unchanged) carries name, uri, digest, content (base64), downloadLocation, mediaType, annotations, and MUST carry at least one of uri, digest, or content.

Rows

Each entry in attributes[] is one row: one attribute, measured for the one target the statement covers. A row is {attribute, basis, result, ...}; attribute, basis, and result are always required. Its shape follows SCAI's attribute-assertion model, with evidence widened from SCAI's single descriptor to an array — the one deliberate deviation from that source.

basis

Every row declares whether a verifier can check it or must trust it:

result

result is one of PASSED, WARNED, FAILED (in-toto Test Result v0.1, unchanged) or NotAvailable, Error, NotApplicable (OpenSSF Scorecard probe outcomes). A row whose result is NotAvailable, Error, or NotApplicable MUST carry reasoning explaining why the row could not be measured, and such a row MUST NOT be read as a pass. This is in-toto's monotonic principle applied to a single row: a policy consuming this predicate should prefer "deny unless a passing, matched row exists" over "deny only if a failing row exists", precisely because an unmeasured row is silent, not clean. What PASSED/WARNED/FAILED mean is attribute-specific — for several attributes (notably fault.*) PASSED means only "measured; see the recorded value", never a value judgment about which value is good. See attributes.md for the per-attribute rule.

Other row fields

Re-derivable is not identical

Decided in v0.1. Some re-derivable rows recompute to the exact same value on any runner: reachability's "does this cwd resolve the hook" is a yes/no fact of the bundle and the agent, independent of hardware; cost.context_tokens under a named tokenizer is one number. Others recompute to a comparable distribution, not the same number: cost.latency_ms measured on a laptop and on a CI runner differ in absolute value while describing the same behaviour. v0.1 keeps two basis values and marks the second case on the row: environmentSensitive: true with environment recorded (at least the platform and CPU count, so a verifier compares like with like). The schema requires both on a measured cost.latency_ms row. A verifier recomputing a row compares the full measurement exactly unless the row is environmentSensitive, in which case it compares unit and that both sides have n >= 1, and leaves the numbers to the consumer's own threshold. A third basis value was considered and rejected: the procedure is re-derivable; only the numbers are not, and that is a property of the row, not of the trust model.

Where the arms live

An efficacy row is a summary; the model outputs it summarizes are a run's actual arms, and a statement never inlines them. A producer measuring efficacy records every arm's output once, as it is produced, and lists that recording under byproducts as a resourceDescriptor with mediaType: "application/vnd.context-report.transcripts+json", bound by digest — never by name or path alone, for the same reason a subject[] entry is never matched by name alone (see Envelope). Judging — deciding whether a given arm's output met a rule's criterion — is a separate step from running the arm, and MAY be redone later against a different judge model without re-running the subject model at all: the transcripts are what make that possible. See ../../run/v0.1/ for the manifest that produces a run's subjects, arms, and transcripts in the first place; this predicate only ever consumes the result.

Applicability by subjectKind

Not every attribute applies to every kind: a prose instruction-file has nothing to execute, a hook script's own text is not injected into the context window. Each section of attributes.md states its "Applies to" list, and the schema enforces the same table at the predicate level: for a (subjectKind, attribute) pair the registry excludes, the row MUST be present with result: "NotApplicable" and a reasoning, never omitted and never PASSED. The table is also shipped as data with the reference producer (applicability-v0.1.json) so a producer emits those rows without reading the prose.

What the verifier emits

A verifier that recomputes rows emits two separate statements, never one:

  1. A recomputation — a second context-report statement against the same subject digest and target, with producer.id set to the verifier, and attributes[] limited to the rows the verifier is able to recompute (basis: "re-derivable" only). A consumer diffs the author's report against this recomputation row by row: matching inputHash values and matching results mean the submission is corroborated; a mismatch on any row is a rejected submission. A recomputation carries no verdict field and MUST NOT be shaped like one — it is exactly this predicate, run again by a second producer.
  2. A verdict — a separate statement, in an unrelated predicate (an in-toto SVR v0.2 verdict, or a VSA using non-SLSA_-prefixed custom levels), whose policy inputs point by digest at both the author's report and the recomputation above.

The two are kept apart because a recomputation is an input to a decision, never the decision itself — mixing them would let a producer's own report re-encode a "pass" through the back door that Purpose above rules out, and would leave a consumer no way to tell "the vendor observed the same facts" apart from "the vendor approved this artifact".

Versioning

Per in-toto convention, 0.X versions are major: fields MAY be added, removed, or change meaning between 0.1 and 0.2 without notice, and the major is carried in the predicateType URI itself (.../attestation/v0.1, not a version field inside the body). A verifier pinning --predicate-type to a specific URI is therefore pinning to a specific schema, by construction. Consumers MUST ignore predicate fields they do not recognize (see Envelope), which is what allows additive changes within a major to stay compatible in practice even though the version number does not promise it.

Each attribute definition in attributes.md carries its own since: vX.Y line, independent of the predicate's own version and independent of any producer tool's version. This is deliberate: OpenSSF Scorecard versions only the tool that runs its checks, not each check individually, so a check's behaviour can change silently under a stable-looking tool version. This predicate does not copy that — an attribute's meaning is pinned by the spec, not by whichever context-report library version happened to compute the row.

Extensions

An attribute name outside the v0.1 registry MUST match ^x-[a-z0-9][a-z0-9._-]*$ (e.g. x-license-check). Extension rows follow every other rule in this document — basis, result, and the inputHash/reasoning requirements apply identically. A consumer that does not recognize an x- attribute MUST ignore the row rather than reject the statement.

Non-goals