# Adversarial Execution Evidence, v0.7

<!-- restatement-banner:begin
     Every figure inside this block is recomputed by scripts/lint_spec_restatement.py and
     is a build failure when it is wrong -- the block asserts facts about two files, and
     the first version of it stated the line count of the file it had not yet joined.
     Do not edit a number here by hand; re-derive with --repin and read what it reports.
     These sentinels are the gate's parse boundary and the reason the banner's own MUSTs
     are not counted as obligations. Removing or moving one is a build failure. -->

> ## THIS DOCUMENT IS NOT THE SPECIFICATION
>
> **The authoritative text is `spec/predicates/adversarial-execution-evidence.md` in the
> `in-toto/attestation` fork**, mirrored byte-identically in the `aee-conformance` repository. That
> document is 2,322 lines, whose sha256 is
> `759d2383e5da36fa509dc335e6159a20b87641b25ebbadcf1676c55d75ffd8b0`; this one is 1,193. They are
> deliberately different documents, and this one is a LOCAL RESTATEMENT maintained alongside the
> authority — not a copy of it.
>
> **Do not cite this file as normative. Do not resolve a disagreement in its favour.** Where this text
> and the authoritative text differ, the authoritative text wins and this file is the thing to fix.
>
> **Why this banner exists.** On 2026-07-30 an audit found this file carries 52 sentences imposing
> `MUST` and 37 imposing `REQUIRED`, sits at a path named `spec/v1/`, nowhere stated that it was not
> the specification, and had **no gate of any kind checking it against the authority** — verified by
> searching `scripts/`, `.github/workflows/` and `.githooks/`, which returned zero references to it.
> A reader had no way to tell it apart from the real thing, and "kept in lockstep by hand" is a
> promise with nothing enforcing it. The counts are over sentences, taken with this banner and the
> fenced examples excluded and wrapped paragraphs rejoined first; a sentence carrying both keywords
> is counted under both.
>
> **What checks it now.** `scripts/lint_spec_restatement.py`, against the pin ledger
> [`adversarial-execution-evidence.pins.json`](adversarial-execution-evidence.pins.json). The ledger
> records the authority by content digest, never by version string or line number, so any edit to the
> authority makes the pin stale and the gate red until a person re-reads both texts and re-pins.
> Every normative sentence below is enumerated there: 8 are anchored to a span of the authority that
> carries that sentence verbatim, and 80 are recorded as unanchored, meaning the authority states
> the obligation in different prose and no mechanical check establishes that the two say the same
> thing.
>
> **The pin does not close the semantic gap, and that is the honest limit of it.** For an unanchored
> sentence this gate proves two things and no more: that the authority has not moved since a person
> last read it against this text, and that every field name the sentence constrains exists in the
> authority at all. It does not prove the obligation is one the authority imposes. Read the authority
> before relying on an unanchored sentence, and read the ledger for which are which.

<!-- restatement-banner:end -->

**predicateType:** `https://in-toto.io/attestation/adversarial-execution-evidence/v0.7`

> **v0.6 makes `basis: substrate` rows substrate-backed at TWO appraisal gates.** GATE 1
> (**coverage validity**, RATS RFC 9334 "Appraisal Policy for Evidence") is a key-independent
> consumption precondition computed from the carried bytes: references resolve and class-match,
> every covering observation payload is canonical `+json` carrying the reserved members with
> `aeeRunBinding` equal to the derived run binding, `method` is capped by the weakest signed
> `aeeMethod`, and `batchRoot` recomputes; a violation makes the attestation INVALID. GATE 2
> (the per-row **evidence tier** `declared`/`unattested`/`attested`, RATS "Appraisal Policy for
> Attestation Results") is the one trust-relative step: the covering signatures verify against a
> consumer-named substrate observation key. The producer's observation vocabulary now travels
> ON-WIRE (`observationVocabulary`: labels, caught subset, JCS digest), so the `result` recompute
> and GATE 1 are pure functions of carried bytes and archived attestations stay verifiable without
> the producer's documentation. The reduction for `result` and the two gates are pinned by the
> normative [`recompute-table.json`](recompute-table.json), which every conforming verifier
> re-encodes. See the Changelog for the v0.3 -> v0.4 -> v0.5 -> v0.6 breaking-change list.

Adversarial Execution Evidence is a signed, offline-verifiable record of a **bounded adversarial
stimulus** run against a system under test (SUT) inside an attested containment substrate - most
commonly an MCP server scanned by one `mlctl gate <server>` run. It carries, in one standalone
in-toto predicate:

- a **fused, recomputable `result`** (`pass` / `pass_indirect` / `degraded` / `fail`) - a deterministic,
  severity-independent reduction of the carried predicate that any verifier re-derives offline from
  the carried rows, the on-wire observation vocabulary, and the coverage maps alone;
- the **observation environment** - digest-pinned references to the substrate, the attack corpus
  (with its embedded, digest-committed class manifest), the catch policy, the enforced network
  posture, the producer's on-wire `observationVocabulary`, and (whenever a row is `basis: substrate`)
  the `runEntropy` run-start beacon the run binding folds in;
- the **coverage bound** - which assessment classes were assessed and an explicit disclosure map for
  every class that was not;
- the **per-attack containment rows** - `attackResults[]`, each carrying the observed
  `containmentObserved` label plus the row's own `basis` (vantage), `method` (directness),
  `actualLayer`, and `observationRefs` binding it to the observation records that cover it;
- the **signed observation evidence** - `observationRecords[]`, each an independently signed record
  of one interception, the armed vantage the run was observed under, or the seal that the vantage
  stayed armed to run-end, carried as a **standard DSSE envelope** `{ payload, payloadType,
  signatures[] }` whose `payload` is the base64 (RFC 4648 §4 standard alphabet) of the EXACT
  canonical `+json` bytes the substrate signed. A verifier verifies the per-record signature over
  `PAE(payloadType, payload)` FIRST, then reads the reserved payload members after.

This document specifies the predicate body. The signing envelope, subject binding, and offline-verify
mechanics are in [signing-envelope.md](signing-envelope.md).

## Purpose

This predicate is the **active-stimulus sibling** to passive runtime monitoring attestations (such as
the in-toto `runtime-trace` family): where a passive trace records an unbounded, monitor-specific log
of whatever happened, this predicate records a *bounded* adversarial campaign - a pinned corpus of
attacks dispatched against the SUT - with per-attack rows, a cryptographically committed coverage
bound, and each observation independently signed.

The design goal is that a consumer can recompute the outcome from the attestation alone, with no call
back to the producer's infrastructure and no dependency on a document that does not travel with the
statement. The outcome is a deterministic function of the carried evidence; the coverage denominator
is committed by digest, so the producer cannot assert it unilaterally; the observation
vocabulary travels on-wire, so the reduction is not something the producer's docs must supply; and
each observation record verifies on its own before it is read. A producer cannot claim more than the
evidence supports, and a producer claiming less is detectable, since dropping an inconvenient
interception changes the committed batch root.

Unlike Simple Verification Results (SVRs), which communicate opaque, delegated policy decisions, this
predicate's `result` is a strictly deterministic, severity-independent reduction. No policy threshold,
severity ruleset, or producer-private logic participates in the reduction; a consumer that wants to
apply its own policy emits its own downstream verification summary over this evidence - this predicate
does not carry one. Verdicts (pass/fail against an organization's policy) are deliberately out of
scope; they belong in a downstream summary predicate computed over this evidence.

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

The on-wire JSON shape is pinned by a draft 2020-12 JSON Schema, the SSOT at
`spec/schemas/v1/adversarial-execution-evidence.schema.json`, a **conformant structural profile** of
the open standard: it gates the field set, primitive types, and the known member set of the
structural objects (rows, records, environment carry `additionalProperties: false`), while the
value vocabularies whose violation is *valid-but-fail-closed at the recompute gate*
(`containmentObserved`, `basis`, `method`) are left OPEN and validated by the verifier, not the
schema - so an out-of-vocabulary or (for `basis`/`method`) absent value is structurally valid and
fail-closes at recompute. `actualLayer` and `networkPosture.posture` remain closed producer
registries, and the predicate object itself is open so a reserved-prefix `aee*` or `evidenceTier`
member is ignored rather than rejected. The **soundness** clauses below - the
JCS corpus-manifest and observation-vocabulary digest equalities, the attack-level coverage
exhaustion, the result recompute, the two consumption gates (coverage validity + evidence tier), the
per-record DSSE proof-of-observation, the run binding, the RFC-6962 batch root, and the
actualLayer/none coherence - are enforced by the producer's `validate()` gates AND the offline
verifier rules, NOT by the schema alone; a schema-valid object is not necessarily a sound artifact.

**I-JSON safe-integer profile (normative).** Every signed surface - the statement payload and every
observation-record payload - is RFC 8785 (JCS) restricted to the RFC 7493 (I-JSON) profile: strings,
integers of magnitude below 2^53, booleans, and null. A duplicate member, a non-integer numeric (a
fractional `1.5`, an e-notation `1e+21`, a non-finite `Infinity`/`NaN`), or an integer at or above
2^53 MUST be rejected by every rail, so producer and verifier in any language derive identical bytes.

**Statement-wide strict I-JSON (normative).** The whole statement is parsed as strict I-JSON: a
duplicate member anywhere in the statement, at any depth and not only inside a covering record payload,
makes the statement malformed. A lenient parser that silently keeps the last of a repeated member would
let two rails disagree on identical bytes, so a verifier MUST reject a duplicate member statement-wide,
fail-closed.

**String well-formedness (normative).** Strict I-JSON also constrains the bytes of every string. A
verifier MUST reject, statement-wide and fail-closed, any statement in which a string literal is not a
well-formed sequence of Unicode scalar values: the statement MUST be valid UTF-8 with no overlong form
and no surrogate encoded directly in UTF-8 (CESU-8); a `\u` escape naming a high surrogate MUST be
immediately followed by a `\u` escape naming a low surrogate, and an unpaired surrogate escape of either
half is malformed; a string MUST NOT contain a raw unescaped character below U+0020; and a `\u` escape
MUST consist of exactly four hexadecimal digits, with no sign, whitespace, or radix prefix, so that a
reader built on a permissive integer parser does not accept `\u+041` where a strict one rejects it. A
lenient decoder does not fail on ill-formed bytes, it substitutes U+FFFD, and every check downstream of
the decode then reads a string the producer never wrote; where a digest is recomputed from decoded
strings rather than compared against carried bytes - which is how the `observationVocabulary` digest is
defined - a producer could otherwise emit ill-formed bytes, derive the digest over the substituted form,
and obtain a statement one conforming verifier calls valid and another calls malformed. A verifier MUST
therefore apply this check to the raw bytes, before any decoded string is read. The profile also
excludes the Unicode noncharacters - the code points U+FDD0 through U+FDEF, and U+nFFFE and U+nFFFF in
every plane - which RFC 7493 section 2.1 forbids in the same sentence as surrogates. A noncharacter is a
valid scalar value that nothing substitutes for, so unlike an ill-formed sequence it is not a cross-rail
decoding split; it is excluded so that a verifier implementing the RFC 7493 label does not reject a
record another verifier accepts, and it is rejected wherever a string literal appears, at any depth and
in both member-name and value position. This rule governs whether the bytes denote admissible scalar
values; the BMP-only rule below governs which of the remaining scalar values may appear.

**Nesting-depth bound (normative).** A verifier MUST reject, fail-closed, a statement whose JSON nesting
depth exceeds 128. Nesting depth is the number of arrays and objects open at a given point, counting the
outermost `{` of the statement as depth 1; scalar values do not increase it. The bound is normative
because it is not a resource limit alone: with no bound stated, implementations pick their own, and two
conforming verifiers then disagree about whether identical bytes are evidence at all over the entire
range between their choices. The counting rule is stated because implementations that increment per
parsed value rather than per open container arrive one level apart from an identical constant. Record
payloads are parsed under the same bound.

**BMP-only string profile (normative).** On every signed surface, object member names and the
`observationVocabulary.labels`/`caught` strings MUST be BMP-only - no supplementary-plane code point,
no surrogate pair. RFC 8785 sorts by UTF-16 code unit, but a verifier that sorts by Unicode code point
orders a name outside the BMP differently (a surrogate-led name sorts first under UTF-16 and last under
code point), so an adversarial producer could mint a payload canonical under one reading and not the
other, splitting `covers` versus `covers-nothing` - attestation-valid versus attestation-invalid on
identical bytes. Restricting the sorted strings to the BMP makes UTF-16 and code-point order coincide,
so the divergence is unconstructible; this is the string half of the safe-integer profile above.
Probity's ASCII vocabulary trivially satisfies it. BMP-only is a verifier rejection obligation, not
producer hygiene: a verifier MUST treat a supplementary-plane member name as making the covering
payload cover nothing, and a supplementary-plane vocabulary entry as making the statement malformed,
the same handling as non-canonical bytes. The UTF-16 sort rule (above) remains the defense in depth:
a verifier orders `labels`/`caught` by UTF-16 code unit, so even against a rail that skipped this
rejection rule, a non-conforming producer's bundle could not split verifier consensus.

**Determinacy is established by vectors (normative honesty).** These bounds close the divergences the
text can foresee: a stated depth, a fixed sort order, a pinned encoding. They do not close the ones it
cannot. Where the text underdetermines a reading and no conformance vector exercises it, two
implementations agreeing on that reading is evidence the text is determinate, not proof of it - the
reading is untested rather than confirmed, and a third implementation could differ there in silence.
Conformance is established by vectors; an agreement no vector has exercised is a candidate for the next
vector, not a settled rule.

All predicate field names are lowerCamelCase.

## Subject

The subject is the **scanned SUT image, by digest** - the thing the evidence is about. It is NOT the
digest of the attack corpus; that is carried in `observationEnvironment.corpus.digest`. Binding the
subject to the scanned image lets an admission controller link the evidence to the exact image it is
admitting. For this predicate `subject` MUST contain exactly one entry on a statement of any basis; a
statement carrying zero or more than one subject is malformed, regardless of whether any row is
`basis: substrate`. Its digest MUST carry a `sha256` value that is already lowercase 64-hex (the run
binding takes it verbatim).

```json
"subject": [
  { "name": "<scanned-sut-name>", "digest": { "sha256": "<64-hex-digest>" } }
]
```

## Run binding

For any statement carrying at least one `basis: substrate` row, the **run binding digest** is the
lowercase 64-hex SHA-256 of the RFC 8785 canonicalization of the object

```json
{
  "aeeBindingVersion": "2",
  "catchPolicy": "<catchPolicy.digest.sha256>",
  "corpus": "<corpus.digest.sha256>",
  "networkPosture": "<sha256 of the RFC 8785 canonicalization of the carried networkPosture object>",
  "observationVocabulary": "<observationVocabulary.digest.sha256>",
  "runEntropy": "<runEntropy.digest.sha256>",
  "subject": "<subject[0].digest.sha256>",
  "substrate": "<substrate.digest.sha256>"
}
```

Every input is run **configuration**, fixed before corpus injection. That is the admission test for
any future input, not a coincidence: the arming record carries this digest inside its own signature
and is signed before injection, so a value the producer could not know then would make the arming
record unsignable, and no outcome of the run can ever appear here.

`runEntropy` is a run-start value the substrate emits and commits inside the arming record's
signature, so two executions sharing every other input still derive distinct bindings. Values are
taken as written (no case-folding, no null fill), and `catchPolicy`, `corpus`, `runEntropy`,
`substrate` and `subject[0]` MUST each carry a lowercase-64-hex `sha256`. So MUST
`networkPosture.digest.sha256`, even though version 2 no longer reads it verbatim, because it is
still compared byte for byte against a record's `aeePostureDigest`. The `observationVocabulary`
digest is the one binding input with no canonicality rule of its own: the vocabulary
digest-integrity check recomputes it from the carried arrays, so a non-canonical value cannot equal
that recompute, and restating the requirement would add a condition that could never be the one to
fail. A statement whose rows are all `basis: artifact` derives no binding and need not carry
`runEntropy`. A verifier derives the digest **from the statement alone**; no field carries it. Every
substrate-signed observation record commits to the run by carrying this digest as its `aeeRunBinding`
member.

The binding is **anti-splice**, not a freshness challenge: a record signed under a different subject,
corpus, catch policy, network posture, observation vocabulary, substrate, or run-start entropy value
cannot be spliced in. It
carries no verifier nonce, and identical-configuration re-runs are distinguished only by the
substrate-emitted `runEntropy` value, so a consumer that must exclude replay of a genuine record into
a later identical-configuration run does so by rejecting reuse of a `runEntropy` value it has already
seen. `aeeBindingVersion` names this construction; exactly ONE is implemented at a time. A future
version that changes the construction (another hash algorithm, additional inputs, multiple subjects)
names a new binding version, and a verifier MUST reject, fail-closed, a binding version it does not
implement rather than attempt more than one construction. An arming record's payload MAY carry an
explicit `aeeBindingVersion` member declaring its construction; a verifier reads it before deriving
the run binding and rejects it fail-closed (the arming record covers nothing) when the value is a
version it does not implement, distinguishably from a run-binding digest mismatch. An absent member
defaults to the **implemented** version rather than to a fixed number, which is what keeps the
declaration optional across a version change; the carried value never drives the derivation (a
verifier always derives under the version it implements, so a record declaring the implemented
version but constructed otherwise still fails on the digest), which keeps the digest authoritative.

### What version 2 changed, and why neither change costs a byte

Both inputs are material the statement already carries, so the wire is unchanged and no new
comparison exists: each closes through the equality every record's `aeeRunBinding` is already put to.

Version 1's `networkPosture` input was the value of that member's own `digest.sha256`, which left the
`posture` string beside it outside every signature. The posture configuration this predicate digests
travels nowhere in the statement, so no verifier can check the string against the digest, and a party
holding only the envelope key could swap one posture value for another, change no digest and break no
signature. Version 2 hashes the carried `networkPosture` **object**, so the string, its pinned digest
and any further member a producer carries there are all inside the binding. The object the binding
covers is the carried one: adding, removing or editing a `networkPosture` member after the arming
record is signed derives a binding the producer's own records do not carry.

`observationVocabulary` was not an input at all. Its `caught` array decides which labels are caught,
and both the recompute and the coverage validity requirements read it, so a producer that narrows the
caught set after the run turns a caught row into a clean one. Nothing resisted that: the vocabulary's
own digest is verified only against the arrays beside it, so it re-derives for free, and no record's
binding moved. Binding the carried digest closes it, since a narrowed vocabulary derives a different
run binding and every record then fails the comparison.

Version 1 is retired with **no alias and no dual-accept window**. A statement built under it derives
a digest no record carries; a record declaring version 1 explicitly covers nothing.

## Predicate

```json
{
  "result": "fail",
  "observationEnvironment": {
    "substrate": {
      "name": "<attestation-A-subject-name>",
      "digest": { "sha256": "<64-hex-substrate-digest>" }
    },
    "corpus": {
      "name": "mcp-cve-corpus",
      "uri": "pkg:probity/mcp-cve-corpus@v1.2",
      "digest": { "sha256": "f8c41f617cfeddb97292339c877f66b9afa8c201592d6625bb6e0fc7d172a927" },
      "manifest": { "classes": { "CO": ["CO-EXFIL-1"] } }
    },
    "catchPolicy": { "digest": { "sha256": "<64-hex-JCS-catch-policy-digest>" } },
    "networkPosture": {
      "posture": "sinkhole",
      "digest": { "sha256": "<64-hex-JCS-posture-view-digest>" }
    },
    "observationVocabulary": {
      "digest": { "sha256": "<64-hex-JCS-vocabulary-digest>" },
      "labels": ["batch_bound", "canary_bound", "egress_captured", "not_exhibited", "payload_bound", "window_bound"],
      "caught": ["batch_bound", "canary_bound", "egress_captured", "payload_bound", "window_bound"]
    },
    "runEntropy": { "digest": { "sha256": "<64-hex-run-start-value>" } }
  },
  "coverage": {
    "assessedClasses": ["CO"],
    "outOfScope": {},
    "routedElsewhere": {}
  },
  "attackResults": [
    {
      "attackId": "CO-EXFIL-1",
      "containmentObserved": "egress_captured",
      "basis": "substrate",
      "method": "intercepted",
      "actualLayer": "policy.egress_sinkhole",
      "observationRefs": [0]
    }
  ],
  "observationRecords": [
    {
      "payload": "<base64std(canonical +json bytes the substrate signed)>",
      "payloadType": "application/vnd.probity.aee-observation.v1+json",
      "signatures": [
        { "keyid": "<hex-key-id>", "sig": "<base64-ed25519-per-record-signature>" }
      ]
    }
  ],
  "batchRoot": "<64-hex-observation-record-batch-merkle-root>",
  "doesNotAssert": [
    "payload-decryptability without the customer recipient key"
  ],
  "executionEnvelope": { "otelTraceId": "<32-hex-w3c-trace-id>" },
  "issuedAt": "2026-06-23T16:08:07Z"
}
```

The `payload` above base64-decodes to the exact canonical `+json` bytes the substrate signed - the
reserved members (`aeeRunBinding`, `aeeKind`, `aeeMethod`) and the readable observation fields live
INSIDE it and are read only after the signature verifies (verify-then-read), never as sibling wire
fields. The `corpus.digest.sha256` above is real: it is the SHA-256 of the RFC 8785 (JCS)
canonicalization of the example's embedded `manifest` object (`{"classes":{"CO":["CO-EXFIL-1"]}}`),
which is exactly how a verifier re-derives it. The `observationVocabulary.digest.sha256` is likewise
the SHA-256 of JCS `{"caught":[...],"labels":[...]}`. The other digests, `payload`, `keyid`, `sig`,
and `batchRoot` are `<placeholders>`.

## Field semantics

### `result` (REQUIRED)

One of `fail`, `degraded`, `pass_indirect`, `pass` (lowercase), ordered `fail` < `degraded` <
`pass_indirect` < `pass`. The fused outcome, defined as a **total, deterministic,
severity-independent** function of the carried predicate (rows + carried vocabulary + coverage maps
ONLY - never `observationRecords`, signature outcomes, or consumer policy), pinned by
[`recompute-table.json`](recompute-table.json). It is the **minimum**, under that order, of three
independent conditions - worst-wins, never evaluation order:

1. **FORCES_FAIL** (contributes `fail`) - some `attackResults` row carries a `containmentObserved`
   label **in the carried `observationVocabulary.caught`** set, a label **outside the carried
   `observationVocabulary.labels`** (fail-closed), OR a **missing or out-of-vocabulary `basis` or
   `method`** (fail-closed, same rule).
2. **COVERAGE_INCOMPLETE** (contributes `degraded`) - `outOfScope` or `routedElsewhere` is non-empty.
3. **INDIRECT** (contributes `pass_indirect`) - some **clean** row (its `containmentObserved` is in
   the carried labels and not in the carried caught set, and it is fail-closed on neither `basis` nor
   `method`) declares a `basis` other than `substrate` or a `method` other than `intercepted`.

A condition that does not hold contributes `pass`. `pass` is coverage-bounded-observed - a statement
about what was assessed, not a guarantee of safety - and `pass_indirect` makes the same coverage claim
resting on at least one observation that was indirect in **vantage** (`basis: artifact`, the executed
artifact's own account of itself) or indirect in **time** (`method: reconstructed`, derived after the
event rather than at it).

**Why the fourth value exists.** Without it the top result is reachable by a statement carrying no
substrate evidence at all. A party holding the enclosing envelope key but not the substrate's
observation key relabels every row clean, moves every row to `basis: artifact`, and drops
`observationRecords`, `batchRoot` and `runEntropy` - none of which a non-substrate row requires. What
it presents is well formed and used to read `pass`. Measured over every finding-bearing vector in the
conformance corpus, that statement is **byte-identical** to one an honest producer with no substrate
vantage emits, so no rule over the carried bytes refuses the first without refusing the second, and
refusing both would remove the producer whose attack classes (prompt injection, tool selection) have
no substrate vantage to observe from. The condition therefore **prices** both below a live
interception rather than claiming to tell them apart.

**Why it is not phrased over the evidence tier.** The tier is key-relative, and `result` MUST be
recomputable from carried bytes alone, so the condition reads the **declared** `basis` and `method`
and nothing the tier reads. The cost is stated rather than hidden: an `unattested` substrate clean row
still reaches `pass`, and that is the one rank of the clean-row ordering no byte-pure function can
express. A consumer crediting any `basis: substrate` row MUST still derive the tier.

The default admission threshold is `result == "pass"`. A consumer MAY accept `pass_indirect`, and one
that relaxes below `pass` MUST additionally key on each clean row's `basis` and `method` and on that
row's derived evidence tier, because below `pass` the ordinal stops distinguishing them.

There is deliberately no severity threshold, no policy ruleset, and no free-text reason field: a
verifier MUST be able to recompute `result` from the bundle alone, and a producer MUST NOT record a
`result` the recompute does not reproduce. A missing expected observation MUST surface as a coverage
gap (the affected class moves to `outOfScope` with a reason) - never as an out-of-band degradation -
so the recompute stays total.

### `observationEnvironment` (REQUIRED)

The digest-pinned containment context the evidence was earned under. Five members are REQUIRED
(`substrate`, `corpus`, `catchPolicy`, `networkPosture`, `observationVocabulary`); a sixth,
`runEntropy`, is REQUIRED exactly when any row is `basis: substrate`. All digests are 64-character
lowercase-hex SHA-256 carried as `{ "sha256": "<64-hex>" }`.

- **`substrate`** - an in-toto
  [ResourceDescriptor](https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md)
  carrying the Attestation A subject reference (`name` + `digest`) of the attested isolation
  substrate the SUT ran inside. This lets the evidence inherit the substrate's trust: the substrate
  SHOULD in turn carry its own attestation (e.g. build provenance for the substrate image).
- **`corpus`** - a locally typed reference to the attack corpus, not a ResourceDescriptor (see
  Descriptor typing below):
  - **`name`** (REQUIRED, string) - the corpus name.
  - **`uri`** (REQUIRED, string) - a locator, RECOMMENDED as a
    [purl](https://github.com/package-url/purl-spec) (e.g. `pkg:probity/mcp-cve-corpus@v1.2`).
  - **`digest.sha256`** (REQUIRED) - the SHA-256 of the **RFC 8785 (JCS)** canonical JSON of the
    embedded `manifest` object. NOT a hash of the corpus file bytes and NOT producer-asserted: the
    pre-image is carried in the bundle, so the digest is re-derivable offline with zero fetch.
  - **`manifest.classes`** (REQUIRED, object) - a map from each assessment-class code to the
    **complete array of `attackId`s** that class defines. An `attackId` MUST NOT appear under more
    than one class. This is the coverage denominator at **attack granularity**: dropping a class OR a
    single attackId changes the manifest hash, which changes `corpus.digest`.
- **`catchPolicy`** - an in-toto
  [ResourceDescriptor](https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md)
  identifying the substrate catch-policy (the JSON that tells the substrate WHAT to catch). Its
  **`digest.sha256`** (REQUIRED) is the SHA-256 of the RFC 8785 (JCS) canonical JSON of that policy, so
  an empty/permissive policy (defenses off) is distinguishable from the enforcing one.
- **`networkPosture`** - the substrate-authoritative egress posture the run was contained under,
  sourced from the substrate daemon, not a runner assertion.
  - **`posture`** (REQUIRED, string) - one of `no_network` (no egress path exists), `allowlist`
    (egress permitted only to a declared destination set), `sinkhole` (egress is accepted and
    diverted to a capture endpoint rather than reaching its destination), or `unsafe_bypass_egress`
    (egress is unrestricted and uninstrumented). The registry is **closed**: an absent, non-string or
    unregistered value makes the statement malformed, fail-closed. A minor version MAY append a value
    and MUST NOT redefine a registered one. Closed rather than illustrative because a consumer is
    invited to coherence-check a substrate row's claimed observation against the posture the run was
    contained under, and no verifier can decide whether an unregistered posture provides an
    interception path at a boundary - an open registry would leave that check permanently unreachable
    while appearing to offer it.
  - **`digest.sha256`** (REQUIRED) - the SHA-256 of the substrate's non-secret posture view (posture,
    allowlists, private-IP blocking, interception flags, the host-scoped secret COUNT), canonicalized
    under RFC 8785 (JCS). It deliberately excludes every secret-bearing field, so it is safe to
    publish and a secret rotation does not change it while a posture change does.
  - The run binding folds in the RFC 8785 canonical digest of this **whole object**, so every member
    carried here is inside the substrate's signature. That is a reason not to extend the object
    casually: a producer-specific member changes a signed digest, and one added after the arming
    record is signed invalidates the producer's own statement.
- **`observationVocabulary`** - the producer's versioned observation label set, carried on-wire so the
  recompute and GATE 1 are pure functions of carried bytes and archived attestations remain verifiable
  after the producer's documentation moves or disappears.
  - **`labels`** (REQUIRED, array of strings) - the complete array of `containmentObserved` values the
    producer can emit, **sorted ascending by UTF-16 code unit (RFC 8785 sec 3.2.3) with no
    duplicates**. Probity's set is
    `["batch_bound","canary_bound","egress_captured","not_exhibited","payload_bound","window_bound"]`.
  - **`caught`** (REQUIRED, array of strings) - the subset whose observation constitutes a caught
    containment event, **sorted ascending by UTF-16 code unit, a subset of `labels`**. Probity's set is
    `["batch_bound","canary_bound","egress_captured","payload_bound","window_bound"]` - i.e. every
    label except the clean `not_exhibited`.
  - **`digest.sha256`** (REQUIRED) - the SHA-256 of the RFC 8785 (JCS) canonicalization of the object
    `{"caught":[...],"labels":[...]}`. A statement violating the sort, subset, or digest obligations is
    malformed.

  The producer's published documentation is commentary on the same vocabulary, never a normative
  input. (The label spellings are Probity producer vocabulary; a different producer carries its own
  labels on-wire under this same member, and a consumer treats a `containmentObserved` label outside
  the *carried* set as fail-closed.)
- **`runEntropy.digest.sha256`** (REQUIRED when any row is `basis: substrate`; OMITTED on an
  artifact-only statement) - the substrate-emitted run-start value folded into the run binding. Its
  pre-image is the substrate's run-start checkpoint; it is an **anti-splice beacon**, not a freshness
  nonce. The pre-image SHOULD additionally fold in a publicly datable value that was unpredictable
  before its round (a drand round output, or an RFC 9334 Section 10.3 epoch identifier), in addition
  to, never in place of, the substrate-unique component, fetched at arming time (never cached), with
  the round reference recoverable via the arming payload. A signature over such a value cannot predate
  its round, so the arming record gains a proven earliest-possible signing time, a floor; `issuedAt`
  stays the asserted ceiling, deliberately not a two-sided proof. The floor bounds recency only where
  consumer policy couples the folded round to its freshness window (the producer selects the round),
  and a beacon inside the producer's own trust domain yields no floor against that producer. Public
  rounds also make independent consumers' `runEntropy`-reuse observations comparable on a shared time
  axis.

**Descriptor typing (normative).** All six members of `observationEnvironment` are descriptor-shaped
and exactly two of them are
[ResourceDescriptors](https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md);
the rule separating them is stated here so a later reader inherits it rather than rediscovers the
question. `substrate` and `catchPolicy` identify a resource and carry nothing beside that identity, so
they take the framework type. The `sha256` REQUIRED on each is a requirement the descriptor
specification explicitly permits a context using the type to impose, and reading a pinned `sha256` off
a descriptor is already what this predicate does in its most load-bearing place, since `subject`
entries are ResourceDescriptors by the in-toto Statement specification and the run binding reads
`subject[0].digest.sha256`. This profile narrows the type on the wire rather than widening it: the
schema SSOT carries `additionalProperties: false` on both members, so the descriptor's other optional
members (`uri`, `downloadLocation`, `mediaType`, `annotations`, `content`) are NOT admitted here, and
no rule in this document would read them. The open standard admits them on a ResourceDescriptor; this
structural profile does not, and a statement carrying one on either member is malformed here.

The other four members stay locally typed, and the reasons are stated rather than left to inference.
**Where a member carries the pre-image its own digest is taken over, that pre-image stays on the
statement's own JSON surface**: `corpus` carries `manifest` and `observationVocabulary` carries
`labels` and `caught`, and the only descriptor member that could hold either is `content`, whose value
is base64. Every byte-level rule above is stated over the statement's JSON - the duplicate-member rule
at any depth, the string well-formedness check applied to the raw bytes before any decoded string is
read, the nesting bound of 128, and the BMP restriction on canonical surfaces. Material inside a
base64 member sits outside all four, so carrying a digest pre-image there would open a SECOND
canonicalization boundary inside a signed statement, in a predicate whose whole encoding profile
exists so that two conforming verifiers cannot disagree about identical bytes. Where a member instead
carries further normative material beside an identity it stays locally typed rather than extending a
descriptor with members of its own - `networkPosture` carries the closed `posture` registry and
`observationVocabulary` carries the label sets the recompute reads - which is the shape the in-toto
`runtime-trace` predicate already uses for `monitor`. `runEntropy` is offered as a reading rather than
as a rule: its digest commits to a substrate-emitted run-start value rather than describing a
resource, so a descriptor is the wrong vessel for it.

### `coverage` (REQUIRED)

Which assessment classes were assessed and how the rest were handled. Together with the
digest-committed `manifest.classes` map, this is the coverage bound the verifier checks by set
equality at the class level AND by attack-level exhaustion.

- **`assessedClasses`** (REQUIRED, array of strings) - the class codes actually assessed in this run.
- **`outOfScope`** (REQUIRED, object) - a map from a class code to a human-readable reason it was not
  assessed (empty `{}` when nothing was out of scope). Its presence makes the run `degraded`, never
  `fail`.
- **`routedElsewhere`** (REQUIRED, object) - a map from a class code to the layer or mechanism that
  covers it instead (empty `{}` when nothing was routed).

Every class code in `assessedClasses`, `outOfScope`, and `routedElsewhere` MUST be a key of
`manifest.classes`, the three sets MUST be pairwise disjoint, and their union MUST equal
`keys(manifest.classes)` exactly. Additionally (attack-level exhaustion), the `attackId` set of
`attackResults` MUST equal the union of `manifest.classes[c]` over every `c` in `assessedClasses`.

### `attackResults` (REQUIRED)

One row per corpus attack dispatched. Each row:

- **`attackId`** (REQUIRED, string) - the corpus attack/vector identifier; a member of the
  digest-committed `manifest.classes`. No two `attackResults` rows may carry the same `attackId`; a
  duplicate is malformed. The attack-level exhaustion check set-compares row `attackId`s, so a
  duplicate would collapse silently under set semantics; uniqueness is enforced before that comparison.
- **`containmentObserved`** (REQUIRED, string) - a label from the carried
  `observationVocabulary.labels`. The single **normative** reading of this value is its membership in
  the carried `caught` set; attribution nuance (which caught rung, what evidenced it) is
  non-normative producer vocabulary that nothing normative reads. Probity's labels:
  - `payload_bound` - the strongest caught rung: an intercepted payload's commitment matched a
    corpus-pinned expected artifact hash. (Attribution strength only; non-normative.)
  - `window_bound` - attributed by the drain-barrier timestamp window.
  - `batch_bound` - caught during a fuzz campaign, not bound to a specific input.
  - `egress_captured` - a credential-exfil attempt captured at the sinkhole boundary.
  - `canary_bound` - attributed by a seeded canary marker surviving in the catch's scrubbed payload.
  - `not_exhibited` - the clean label: the attack was dispatched but the SUT never exhibited the
    dangerous behavior, so there was nothing to contain. NOT a containment failure and NOT an evasion.
    (This is Probity's single clean-label spelling; there is no `no_egress`/`uncontained` alias.)
- **`basis`** (REQUIRED, string) - the observation's **vantage**, closed vocabulary
  `{substrate, artifact}`, fail-closed:
  - `substrate`: every input the row's claim depends on was obtained at a vantage the executed
    artifact could neither forge nor suppress (a network boundary, syscall supervision, a
    hypervisor's read of guest state). An input is artifact-sourced when the claim relies on a channel
    the artifact can populate arbitrarily without performing the claimed event (its stdout/stderr,
    exit status, self-emitted logs); an egress capture is NOT artifact-sourced even though the packet
    bytes were artifact-authored, because the artifact cannot cause the boundary to record an egress
    without performing one.
  - `artifact`: at least one input the claim depends on derives from output the executed artifact
    itself produced.

  `basis` is the vantage of the claim's **weakest input**: a derived observation inherits `artifact`
  from any artifact-sourced input it consumed. A producer MUST NOT declare `basis: substrate` on a
  row it cannot cover under GATE 1 (Coverage validity): such a row is not merely mislabeled, it makes
  the attestation invalid.
- **`method`** (REQUIRED, string) - the observation's **directness**, closed vocabulary
  `{intercepted, reconstructed}`, fail-closed:
  - `intercepted`: the claim rests on events captured as they occurred. On a clean row, a live
    capture vantage was armed for the attack and no capture was attributed to it.
  - `reconstructed`: the claim derives from state examined after the fact (a snapshot-to-snapshot
    diff), which can miss a transient raised and undone between the states compared.

  Like `basis`, `method` composes by weakest input. Post-hoc decode of an event stream captured as it
  occurred does not demote a row, provided the capture channel was armed for the claimed event class
  before the event; a row fusing a live capture with after-the-fact state examination is
  `reconstructed`. `method` describes how the observation was made, not how the row was attributed to
  its attack. Both fields are REQUIRED on every row and both vocabularies are closed: a missing value,
  or any value outside them, is fail-closed exactly as an out-of-vocabulary `containmentObserved`
  label is - the row forces `result` to `fail` and can support nothing stronger. The retired 0.4
  values `substrate_observed` / `artifact_reported` / `inferred` are out-of-vocabulary, with no alias
  (`inferred` has no successor because it conflated the two axes).
- **`actualLayer`** (REQUIRED, string) - which enforcement layer acted on the row's containment
  event, drawn from the registered layer set ([vocabulary registry](vocabulary-registry.md)), OR the
  literal string **`none`**. A row whose `containmentObserved` is a label-not-in-caught (a **clean
  row**: nothing acted) MUST carry `none`; `none` is also valid on a caught row and there states the
  event was observed but no enforcement layer acted (a passive tap, a monitor-only vantage), so
  enforcement role travels here and only here and `basis` never has to encode who could act.
  `actualLayer` is REQUIRED on every row; a **missing** member is a MALFORMED statement (a different
  altitude than the fail-closed row members the recompute reads), not weak evidence.
- **`observationRefs`** (REQUIRED on `basis: substrate` rows, array of non-negative integers) -
  indexes into `observationRecords` binding this row to the records that cover it. An `interception`
  index MAY be referenced by more than one row. A producer MUST NOT reference a record from a row
  whose attack the record's committed payload does not evidence. On a row declaring
  `attribution: pinned` that obligation is checkable and is checked, by the coverage validity
  requirement stated above: the corpus declares what the attack's interception commits to and the
  verifier compares. On a row declaring `paired` it remains an obligation outside every gate,
  because no validity requirement, recompute input or tier evaluation reads it there, and a
  conforming verifier neither can nor may invent an evidencing heuristic in its place. The line
  between the two is exactly the line the corpus draws by carrying an expectation or not.
  `arming` / `sealed` / `examination` indexes are run-level and MAY be shared by every row earned
  under them. Wherever `observationRefs` is present - on any row, regardless of `basis`, and including
  rows on which nothing normative reads it - every index MUST be in range for `observationRecords`. An
  out-of-range index is a structural integrity fault that makes the statement malformed, fail-closed
  and independent of any gate, so a reference that does not resolve is never silently ignored.
- **`observationSelectors`** (OPTIONAL, array of strings) - producer-defined tokens positionally
  parallel to `observationRefs`, each naming the sub-observation within a referenced record's payload
  this row rests on. Token content is producer vocabulary; nothing normative reads it, and selector
  presence or absence changes no gate outcome.

**Consumer strength orderings (non-normative guidance; not part of the recompute).** The two axes
bind a consumer's confidence on opposite sides. `basis` bounds a `fail` over its supporting rows
(those forcing `result` to `fail`): a `fail` whose supporting rows are all `artifact` is a weaker
claim than one carrying a `substrate` row, and a consumer MAY reject it; a `fail` from a
(`substrate`, `reconstructed`) row is still an observation the artifact could not manufacture, weaker
than an interception only in that it was derived after the fact. `method` bounds a `pass`: a `pass`
whose clean rows are all `attested` at (`substrate`, `intercepted`) - each covered by both the run's
`arming` record and a covering `sealed` record - makes the strongest absence claim this predicate can
carry; a `pass` resting on any `reconstructed` clean row tolerates transients between the observed
states; a `pass` resting on any `artifact` clean row, or an `unattested` substrate clean row, is
self-reported absence, the weakest. Only rows deriving `attested` occupy the `substrate` rank; an
`unattested` substrate row ranks with `artifact` in both orderings. A consumer **MAY reject, NEVER
downgrade**: declining to credit a rank is not a rewrite of signed bytes.

**Field partition (delta-I: which identity's signature backs which field).**
Substrate-covered - through GATE 1 and the evidence tier: `basis` and `method` on rows deriving
`attested`, and the content of every verified observation record. Producer-asserted - backed only by
the enclosing envelope: `containmentObserved` labels and their attribution nuance, `basis`/`method`
on `artifact` rows, `actualLayer`, `coverage`, `doesNotAssert`, and the assembly of the predicate
itself. The two are not composable: a per-observation property the recompute or the documented
consumer gating reads travels ON the row as a required member (with a closed vocabulary, fail-closed
on missing or unknown values); run-level pins in `observationEnvironment` never substitute for a
row-level property, and the run-level `arming`/`sealed` instruments back a clean row's claim *through*
the row's own `observationRefs` without substituting for a row member. Which keys count as substrate
observation keys is consumer key policy; the substrate observation key MUST NOT be accessible to the
subject artifact and SHOULD be held apart from the producer's assembly plane. Where it is so held, the
tier defeats a pipeline with no substrate in the loop, cross-configuration splices, record drops, and
method inflation; where one party holds both keys (the single-root deployment), the tier instead
defeats only a keyless downstream tamperer - a substrate operator who signs false evidence remains
outside this predicate's threat model, as for every self-asserted field.

### `observationRecords` (OPTIONAL)

The signed observation records the containment rows bind to. OPTIONAL: an artifact-only statement
carries none (and then carries no `batchRoot`). **Each entry is a standard
[DSSE](https://github.com/secure-systems-lab/dsse) envelope** carrying the EXACT signed bytes of one
observation - there is NO parallel readable object; the readable fields live inside the signed
`payload` and are read only after the signature verifies. Each entry:

- **`payload`** (REQUIRED, string) - the **base64 (RFC 4648 §4 standard alphabet)** of the exact
  canonical `+json` bytes the substrate signed. Any record used to cover a `basis: substrate` row
  MUST base64-decode to a JSON object that is canonical per RFC 8785 and valid I-JSON per RFC 7493,
  carrying these reserved top-level members (a record whose payload is not so parseable, or whose
  media type is not `+json`, covers nothing):
  - **`aeeRunBinding`** (string) - the run binding digest defined under Run binding.
  - **`aeeKind`** (string) - one of:
    - `interception` - per-event capture; covers **caught** rows. `aeeMethod` MUST be `intercepted`.
    - `arming` - run-level: a live, cooperation-independent capture vantage was armed for the run
      before corpus injection. Payload MUST carry `armedAt` (under the timestamp profile `issuedAt`
      defines, no later than `issuedAt`) and `aeePostureDigest` (equal to the pinned `networkPosture`
      digest); `aeeMethod` MUST be `intercepted`. Covers **clean** rows (with a covering `sealed`
      record).
    - `sealed` - run-level: the vantage stayed armed to run-end. Payload MUST carry `aeeStillArmed`
      (boolean, MUST be `true` to cover), `aeeDropCount` (integer; MUST be `0`, or `0 <= aeeDropCount
      <= aeeDropBound` where `aeeDropBound` is a producer-declared integer in the SAME payload), and
      `aeePostureDigest` (equal to BOTH the pinned `networkPosture` digest AND every covering `arming`
      record's `aeePostureDigest`); `aeeMethod` MUST be `intercepted`. Covers **clean** rows (with an
      `arming` record).
    - `examination` - the substrate examined artifact-independent state after the fact. `aeeMethod`
      MUST be `reconstructed`. Covers **reconstructed** rows.
  - **`aeeMethod`** (string) - `intercepted` or `reconstructed`, how the substrate observed, stated
    inside the signature (it caps the row's `method`, see GATE 1).

  An `arming` record's payload MAY additionally carry three reserved members that chain runs under
  the same substrate key: `aeeRunSeq` (a positive safe-range integer), `aeePrevRunBinding` (the
  lowercase 64-hex run binding digest of the predecessor run, absent exactly when `aeeRunSeq` is
  `1`), and `aeeChainScope` (the population the sequence counts, declared as a duplicate-free array
  of dimension tokens drawn from the closed vocabulary registered below, sorted in the same canonical
  order as `observationVocabulary.labels` (UTF-16 code-unit order, RFC 8785 section 3.2.3);
  REQUIRED whenever `aeeRunSeq` is present). The chain is always structurally under one substrate
  key; each token names a further within-key partition attribute already carried elsewhere in the
  attestation and fixes where a consumer reads that attribute's value. The declared array is the
  *dimension set*; the *evaluated tuple* is the projection of the substrate-key value and each
  declared token onto its registered attribute value for this run (computed, never carried). The
  recommended minimum is `["subject"]`; the empty array is the single global per-key counter that
  makes every rule below vacuous and leaks the producer's total run volume across customers.

  The `aeeChainScope` vocabulary is closed and each token pins a projection to a value already carried
  on the wire: `subject` to `subject[0].digest.sha256`, `corpus` to
  `observationEnvironment.corpus.digest`, and `networkPosture` to `networkPosture.digest.sha256`. The
  substrate key is the structural outer axis and is never a token. Values are not carried in the
  member; a consumer projects each declared token onto its registered field for this run. Minor
  versions MAY append tokens (each with a pinned projection) and MUST NOT redefine an existing one; an
  unrecognized token fails closed, as every closed vocabulary in this spec does.

  Within one attestation these members are syntax-checked in the reserved-member walk and nothing else
  normative reads them (GATE 1, the recompute, and GATE 2 are unchanged). A violation of the syntax
  rules - a non-positive or non-integer `aeeRunSeq`, a malformed `aeePrevRunBinding`, a missing
  `aeeChainScope` when the sequence is present, a non-array `aeeChainScope`, an array carrying a token
  outside the registered vocabulary, an array not in canonical order (the same canonicality rule as
  `observationVocabulary.labels`: UTF-16 code-unit order, duplicate-free), or any of the three present
  without `aeeRunSeq` - is handled as any reserved-member violation: the record covers nothing.

  Across attestations these members are consumer policy over whatever set the producer publishes. A
  consumer compares each attestation's declared dimension set against the set its policy demands: an
  equal set is admissible; a strictly finer set (a superset of dimensions) is scope-narrowing,
  fragmenting every run into a singleton chain so no gap, fork, or duplicate genesis can arise and the
  chain proves nothing; a strictly coarser set (a subset of dimensions) pools distinct subjects, so a
  withheld run of the demanded subject is deniable as a sibling's private run and a sibling's run can
  occupy the withheld sequence position. A consumer that has demanded a scope admits only the equal
  set, neither finer nor coarser. Among admitted attestations the rules key on the evaluated tuple,
  not the token set: a skipped `aeeRunSeq` under one tuple is a gap; two under one tuple with equal
  `aeeRunSeq` are a fork; a shared `aeePrevRunBinding` is a shared predecessor; two genesis records
  (absent `aeePrevRunBinding`) under one tuple are equivocation of the same grade as a shared
  predecessor. Keying on the tuple is load-bearing: genesis-per-subject-value is the normal case, and
  only a second genesis under an identical tuple is a reset. A chain reset is not a fresh start.

  The members claim ordering under the substrate key, nothing more (commit-before-outcome holds only
  combined with the run-entropy floor or an external registration receipt); a numeric gap is
  unexplained absence, never fraud evidence in itself (crashed, private, and discarded runs all produce
  gaps innocently). Even a contiguous, fork-free, correctly-scoped chain does not prove population
  completeness: a producer may still mint a dense, gap-free set of passing runs after the fact. Fork
  consistency among the published set is the ceiling of what any self-contained attestation set
  establishes; the demand-disclosure yield is that a consumer policy MAY require a contiguous, fork-free
  chain over the runs offered to it. The external completion is a registration receipt (committing each
  arming record to an append-only transparency log at run start, e.g. SCITT, RFC 9943, with COSE
  receipts, RFC 9942), deliberately outside this predicate.

  A record violating any constraint of its declared `aeeKind` covers nothing, and the constraints are
  read on every carried record of a covering kind rather than only on the records some row resolves.
  A constraint evaluated only where a row points is a constraint whose subject the producer chooses:
  a substrate signs a `sealed` record reporting its moat down, the producer carries that record and
  points the row at a second seal, and the run reads clean with the record that says otherwise sitting
  in the statement and inside `batchRoot`. A record whose `aeeKind`
  the consumer does not recognize covers nothing and is otherwise ignored, while still contributing its
  leaf to `batchRoot` - an unrecognized kind can only weaken, never strengthen, a row (candidate
  future kinds, informatively: a hardware-quote kind binding the vantage to a measured platform, and a
  `registration` kind carrying a transparency-service receipt over the arming record). The `aee`
  member prefix is reserved for future versions; everything else in the payload stays producer
  territory. Reserved members inside a producer-defined signed payload follow the registered-claims
  lineage (JWT RFC 7519 Section 4; EAT RFC 9711 in the RATS family; OCI annotation prefix
  reservation; the `+json` rule per RFC 6839 Section 3.1), informative citations locating the
  pattern, not importing any cited standard's rules, with two deliberate departures: unrecognized
  or colliding `aee*` members are fail-closed (they can only weaken coverage, never create it) where
  JWT ignores unknown claims, and verify-then-read is normative here. What an `interception` record carries is a commitment to an intercepted payload rather
  than the payload itself, keeping the attestation publishable rather than a sensitive-data store. The
  Probity producer's concrete observation-payload field profile (the readable fields beside the
  reserved members) is specified in [catch-record.md](catch-record.md).
- **`payloadType`** (REQUIRED, string) - a **producer-defined media type ending in `+json`**. It is
  the second input to the PAE pre-image, so a differing value changes the signed bytes. The Probity
  producer emits the constant `application/vnd.probity.aee-observation.v1+json`.
- **`signatures`** (REQUIRED, array, `minItems: 1`) - each element `{ "keyid": <hex>, "sig":
  <base64-ed25519> }`. The signature is over `PAE(payloadType, b64decode(payload))` (DSSE
  Pre-Authentication Encoding). A record verifies when at least one element validates under a pinned
  key. A `keyid` is an unauthenticated lookup hint that selects WHICH pinned key to try, never the
  check itself.

**Verify-then-read.** A verifier processes each covering record as: (1) validate a `signatures[]`
element over `PAE(payloadType, b64decode(payload))` under the pinned substrate observation key; (2)
`record = json.loads(b64decode(payload))`; (3) assert the decoded payload is its own RFC 8785 (JCS)
canonical form and valid I-JSON; (4) read the reserved members and observation fields from `record`.
All bindings (run binding, kind constraints, method cap) operate on the DECODED record, never on wire
siblings.

### `batchRoot` (REQUIRED when `observationRecords` is non-empty)

An RFC 6962 Merkle root over the observation records, SHA-256, with **domain-separated hashing**: each
leaf is `H(0x00 || the record's DSSE PAE bytes)`, each internal node is `H(0x01 || left || right)`,
the tree built by the **RFC 6962 recursive largest-power-of-two split** - never by duplicating a
trailing node to pad the leaf count - leaves in `observationRecords` array order, a single-record
tree's root its leaf hash, and an empty array with no root. **Two byte-identical entries make the
attestation invalid** (duplicate-leaf REJECT: a record's canonical identity is its leaf hash). Carried
once at the predicate level; a `batchRoot` that does not recompute over the carried records makes the
attestation invalid, and a `batchRoot` present with no records is likewise invalid. Because a clean
run's committed set includes its `arming` and `sealed` records, absence evidence cannot be dropped
without changing the root.

> **Fold note (batch-root n=5 non-divergence).** An earlier concern held that the naive
> "promote the unpaired odd node" fold **diverges** from the RFC 6962 recursive split at five leaves.
> That premise is **FALSE**: the two folds were empirically verified byte-identical for every leaf
> count on all Probity rails. The RFC 6962 recursive split is nonetheless pinned as the single
> normative construction, and the only v0.6 delta to the batch-root rule is the **duplicate-leaf
> reject** plus the **PAE-bytes leaf content** (`H(0x00 || PAE(payloadType, payload))`, where v0.3
> hashed the raw canonical bytes); the fold arithmetic itself is unchanged and is NOT rewritten on any
> rail.

> **Why "never duplicate" is load-bearing, precisely.** CVE-2012-2459 (Bitcoin, 2012, verified against
> `bitcoin/bitcoin`'s own `src/consensus/merkle.cpp` commentary and the Bitcoin Optech writeup) is a
> Merkle-tree malleability / invalidity-caching netsplit: pad an odd transaction list by *duplicating*
> the last entry, so an invalid block (the duplicate double-spends) shares a root with the valid block
> it was built from, and a node that caches the invalid form by root refuses the valid form until
> restart. It is **not** a second-preimage attack on the hash function — that is a separate property,
> and the one the `0x00`/`0x01` domain-separation prefixes above defend. The odd-node rule here defends
> against duplication-based root collisions specifically: promotion carries the unpaired leaf's digest
> forward *unchanged*, so an odd batch of N records and a genuinely-duplicated batch of N+1 records
> (the N+1th a byte-identical copy of the Nth) never share a root — and the duplicate-leaf REJECT rule
> above closes the remaining case, an honestly-even batch that happens to carry two identical entries.

### `doesNotAssert` (OPTIONAL)

A machine-readable **negative-claims list**: short strings stating what this evidence explicitly
CANNOT claim, so a consumer does not over-read it (e.g. a `--recipient` run states it does not assert
payload-decryptability without the customer key). Advisory: a verifier MUST NOT require it and nothing
in it participates in the recompute or the binding rules. `doesNotAssert` is the **single canonical
spelling**; the earlier snake_case `does_not_assert` is rejected with no alias (two accepted spellings
would mean two canonicalizations for the same content). Absent when the producer asserts no explicit
negative claims.

### `issuedAt` (REQUIRED)

When the producer signed this evidence bundle, carried as the in-toto framework's
[`Timestamp`](https://github.com/in-toto/attestation/blob/main/spec/v1/field_types.md) field type.
REQUIRED in v0.6 (was OPTIONAL in v0.3). Distinct from the observation timestamps inside
`observationRecords`; an `arming` record's `armedAt` MUST be no later than `issuedAt`. Its purpose is
also consumer-side **freshness gating**: an admission controller MAY deny evidence older than its
window, and a consumer's policy MAY bound a named substrate observation key with a validity window
checked against `issuedAt`.

**Timestamp profile (normative).** `Timestamp` requires RFC 3339 in the UTC timezone, and this
predicate pins the two choices that type leaves open. A statement is canonicalized and digested as its
bytes, so no verifier may normalize the field before reading it and the admissible set has to be
written down; left open, one rail is quietly stricter than another and the divergence surfaces only
when a statement crosses between them. The date-time separator and the zone designator MUST be
uppercase - never the lowercase `t` and `z` that
[RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) also admits - and the zone designator MUST be `Z`,
`+00:00`, or `-00:00`, never a non-zero offset such as `+05:00`. `-00:00` is admitted rather than
excluded because RFC 3339 section 4.3 gives that spelling the meaning that the instant in UTC is known
while the offset to local time is not, which describes where the producer stood and not when it
signed, and the instant is the only thing this predicate reads from the field. A statement whose
`issuedAt` is absent, is not RFC 3339, or is RFC 3339 outside this profile is malformed. `armedAt`
carries this same profile, defined here and cited from the arming record so the two fields cannot
drift apart.

### `executionEnvelope` (OPTIONAL)

The tool-call OTel correlation envelope. When present, **`otelTraceId`** (REQUIRED within, 32-char
lowercase-hex W3C trace-id) is the only gated key; a cross-runtime emitter MAY attach further
correlation fields. OPTIONAL for producer neutrality.

### `_ext` (OPTIONAL)

The in-toto extension-field convention: a free-form object for producer-scoped extensions that are
deliberately **out of core**. A verifier MUST NOT require `_ext` and MUST ignore extensions it does
not recognize; nothing in `_ext` participates in the recompute or the binding rules. No predicate
member (including any `_ext` sibling) may begin with the reserved `aee` prefix.

#### `_ext.run_fidelity` (Probity extension, OPTIONAL)

The run-level rollup of the per-observation **EvidenceFidelity** signals - the aggregate CONFIDENCE
and COMPLETENESS of the run's attribution, so a buyer's OPA/rego policy can gate on evidence QUALITY,
not just structural validity. It is Probity-namespaced: a generic verifier ignores it; a Probity
verifier MAY apply the OPTIONAL recompute rule below. Shape:

- **`clean_drain`** (bool) - true when the run's final signed `quarantine.coverage` observation record
  closed with `status == "final"` and `in_flight == 0 && queued == 0`: the arm provably drained every
  admitted catch, so no attribution was silently lost mid-run.
- **`task_resolution_counts`** (object) - a HISTOGRAM (never a min) over the per-catch owning-task
  resolution mode: `{ "4-tuple": n, "dst-fallback": n, "dst-only": n }`, counted across the run's
  `vmi.attribution` observation records.
- **`verity_device_pairing`** (string, OPTIONAL) - the run-level dm-verity device-pairing confidence
  (`verity-exact-root-digest` or `verity-heuristic-size`), riding the final signed
  `quarantine.coverage` record's `coverage.verity_device_pairing`; present ONLY on a verity-anchored
  run. A `host-block`-sourced (copied-up) ModelBOM entry is authoritative over this run scalar for its
  own attribution (see [ml-bom.md](ml-bom.md)).

All three fields are RE-DERIVABLE from the signed `observationRecords` alone. **Optional recompute
rule (Probity verifier).** `_ext.run_fidelity` is NEVER trusted as asserted: a Probity verifier
RE-DERIVES all three from the verified observation records (which ride inside `observationRecords`,
covered by `batchRoot`) and asserts equality, so a producer cannot inflate its own evidence quality.
An absent `_ext.run_fidelity` is sound; a PRESENT one that does not match the recompute is rejected.

## Verification (third-party, offline, two gates)

Two expectations are consumer policy, resolved outside the attestation and never read from it: which
keys count as substrate observation keys (GATE 2's input), and which corpus and substrate this
consumer expects. A consumer MUST pin, out of band, the `expectedCorpusDigest` and
`expectedSubstrateDigest` for the deployment it is admitting into (a bundle's internal exhaustion is
meaningless unless the consumer pins WHICH corpus and substrate it was earned under) and assert them
at the consumer-policy step after GATE 2. The anchor comparison is deliberately not a validity gate:
GATE 0, GATE 1, and the recompute are functions of carried bytes alone and hold identically for every
consumer, while the expected corpus and substrate differ per consumer. An anchor-mismatched
attestation is valid evidence about the wrong context, and it is NOT ADMITTED, exactly as an
attestation whose covering signatures fail is not admitted. Every verification surface exposes one
consumer-facing admission result, `admitted` = validity AND tier-policy satisfaction AND
anchors-satisfied-when-supplied, and binds its exit status to `admitted`, never to bare validity, so
a result-only consumer cannot read a valid-but-wrong-context attestation as admissible. The exit
binding applies whenever a consumer policy or anchors are supplied; a bare conformance replay with
no policy binds to validity alone; with no policy there is no admission decision to bind. After the
envelope checks in [signing-envelope.md](signing-envelope.md), a verifier - with zero network
access - proceeds through GATE 0, GATE 1, the recompute equality, and GATE 2 (the four byte-pure
steps first, then the trust-relative stage).

### GATE 0 - statement well-formedness

The parsing rules and field shapes: the predicate is its own RFC 8785 (JCS) canonical form and valid
I-JSON (no duplicate members, no unsafe integer); the closed registries (`actualLayer`,
`networkPosture.posture`) validate, while `containmentObserved`/`basis`/`method` vocabulary membership
is deferred to the recompute gate (fail-closed on an out-of-vocabulary or absent value), not gated
here; `subject` has exactly one entry with a lowercase-64-hex `sha256`; each run-binding digest input
read verbatim is lowercase 64-hex, and so is `networkPosture.digest.sha256`, which version 2 of the
binding no longer reads verbatim but which is still compared byte for byte against a record's
`aeePostureDigest`; `issuedAt` is present and inside the timestamp profile; a
`basis: substrate` statement carries `runEntropy`; and `actualLayer` is present on every row (a
missing member is malformed here, not a fail-closed row). Coverage integrity is checked here:
canonicalize `manifest` under JCS, hash, assert
equality with `corpus.digest.sha256`; assert the class sets are pairwise-disjoint and set-equal to
`keys(manifest.classes)`; and, at ATTACK granularity, assert the `attackId` set of `attackResults`
equals the union of `manifest.classes[c]` over `c in assessedClasses`. Canonicalize
`{"caught":[...],"labels":[...]}` and assert equality with `observationVocabulary.digest.sha256`, and
assert `labels`/`caught` are sorted ascending by UTF-16 code unit, dupe-free, and `caught` a subset of `labels`.

### GATE 1 - coverage validity (RATS "Appraisal Policy for Evidence")

A **key-independent consumption precondition** computed from carried bytes (reads record payloads, but
never signatures or consumer policy). A consumer that consumes `result`, credits any row, or applies
either strength ordering MUST evaluate GATE 1 first; on failure the attestation is INVALID and its
`result` MUST NOT be consumed - the same handling as any malformed statement. For every `basis:
substrate` row:

1. `observationRefs` is non-empty and every index is in range for `observationRecords`.
2. **Class-match:** a caught `intercepted` row references at least one `interception` record; a
   `reconstructed` row references at least one `examination` record; a clean `intercepted` row
   references at least one `arming` record AND at least one covering `sealed` record. A row
   fail-closed on `containmentObserved`, `basis`, or `method` cannot class-match and is therefore
   INVALID.
3. Every referenced payload parses as a canonical `+json` I-JSON object carrying the reserved members,
   and its `aeeRunBinding` equals the run binding derived from this statement.
4. The row's `method` is no stronger than the weakest signed `aeeMethod` across its COVERING records
   (records that cover nothing do not participate in the cap).
5. `batchRoot` recomputes over `observationRecords` (RFC 6962, domain-separated, duplicate-leaf
   reject) - a statement-level check run whenever records are present.
6. Every carried record that binds to this run and whose `aeeKind` names a covering kind
   (`interception`, `arming`, `sealed`, `examination`) satisfies every constraint of that kind,
   whether or not any row resolves an index to it - a statement-level check, and the universal
   partner of the class-match rule above, which asks only whether a covering record is present and
   so says nothing about the failing ones carried beside it. The kinds registered as covering
   nothing and the kinds a verifier does not recognize are unaffected: neither carries a constraint
   that could be violated.

### Recompute equality

Re-derive `result` by the [`recompute-table.json`](recompute-table.json) reduction over the rows, the
carried vocabulary, and the coverage maps, and assert it equals the recorded `result`. This is a
function of the carried predicate alone; it never reads `observationRecords`, signature outcomes, or
consumer policy, so a `result` that varied with the consumer's trust anchors would not be recomputable.

### GATE 2 - evidence tier (RATS "Appraisal Policy for Attestation Results")

Given a valid attestation, a consumer MUST - before crediting any `basis: substrate` row or applying
either strength ordering - derive a per-row **evidence tier**: a `basis: artifact` row (and a row
fail-closed on `basis`) is `declared`; a `basis: substrate` row is `attested` when every covering
record's signature verifies against a key the consumer's policy names as a substrate observation key,
and `unattested` otherwise. **No TOFU:** a consumer with no policy-pinned substrate root MUST treat
every `basis: substrate` row as `unattested` and MUST NOT infer the substrate root from the predicate.
The tier is total and deterministic given the consumer's key policy; it NEVER alters `result`. A
carried predicate member named `evidenceTier`, or any predicate-level member beginning with the
reserved prefix `aee`, MUST be ignored and MUST NOT alter the derivation.

Finally the consumer-policy step: when the consumer supplies anchors (it MUST for admission
decisions), assert `corpus.digest.sha256 == expectedCorpusDigest` and `substrate.digest.sha256 ==
expectedSubstrateDigest`; a mismatch fails `admitted`, never validity. Confirm the `subject` digest
is the SUT image the verifier intends to admit and surface posture + coverage + the per-row tier for
the consumer's admission policy. The surfaced report carries both the byte-pure facts (validity,
`result`) and the consumer-relative facts (tier, anchor comparison) with `admitted` as their
conjunction. No live execution environment and no network access are required.

## Named non-claims

- **COV-1 (coverage-bounded-observed).** A `pass` is a statement about what was assessed under the
  pinned corpus and catch policy; it makes no general safety claim.
- **COV-2 (run-population completeness is out-of-wire).** A clean `(substrate, intercepted)` row's
  `pass` is a claim about the run THIS attestation carries - that a live vantage was armed and stayed
  armed and no interception was attributed to the row - never a claim about a run population. Nothing
  in this predicate proves that other runs of the same configuration did not occur or were not
  withheld; run-population completeness (via a run ledger or monotonic counter) is a consumer or policy
  concern.
- **COV-3 (no cross-predicate composition).** This predicate makes no cross-predicate claim: composing
  it with a sibling execution predicate (for example a runtime trace of a different execution) does not
  yield end-to-end coverage, and a consumer MUST NOT infer a composite guarantee unless its policy
  binds both attestations to the same execution (a shared subject digest and run identifier).

## What a verifier can and cannot independently check (honesty)

- A verifier CAN confirm the evidence is authentic, bound to its subject image, and signed by a
  trusted key; recompute the `result`, the coverage-integrity binding, and the on-wire vocabulary
  digest; evaluate GATE 1 (coverage validity) from the carried bytes and derive the GATE 2 tier
  against its own key policy; validate each observation record's DSSE signature and the drop-evident
  batch root; and read the observation environment exactly as recorded.
- A verifier CANNOT, from the evidence alone, confirm that the corpus content behind `corpus.digest`
  is genuinely adversarial (it confirms the *manifest* commitment and checks corpus identity against a
  consumer-pinned digest, not corpus *quality*), nor that the assessment taxonomy is complete.
- A verifier CANNOT distinguish a **genuine evasion from a benign no-op** beyond what the substrate's
  sensors observed under the pinned catch policy: it trusts the substrate's sensor efficacy and the
  producer's taxonomy, and is honest only within the coverage the anchors pin.
- Coverage is only as trustworthy as the named substrate observation key's un-compromised lifetime.
  Where one party holds both the assembly and the observation key (the single-root deployment), that
  single trust root is a single point of total failure: a **compromised or dishonest key-holder** can
  mint any coherent bundle, as with every signed attestation. The soundness rules constrain what a
  validly-signed bundle can *claim and still verify*, not what a key-holder can sign; rejecting an
  under-defended posture (`unsafe_bypass_egress`) is the consumer admission policy's job.

## Related standards

- **[in-toto attestation](https://github.com/in-toto/attestation)** - this predicate is proposed in
  the in-toto attestation namespace as the **active-stimulus sibling** to the passive `runtime-trace`
  family: `runtime-trace` carries unbounded, monitor-specific logs; this predicate carries a bounded
  adversarial campaign with per-attack rows, an on-wire observation vocabulary, and a cryptographic
  coverage bound.
- **[RATS (RFC 9334)](https://www.rfc-editor.org/rfc/rfc9334.html)** - the two-gate structure is the
  RATS appraisal split: GATE 1 (coverage validity) is the Verifier's key-independent **"Appraisal
  Policy for Evidence"**; the per-row evidence tier is the Relying Party's trust-relative **"Appraisal
  Policy for Attestation Results"**, and the tier names `attested`/`unattested`/`declared` align to
  that vocabulary rather than minting bespoke terms.
- **in-toto training / build provenance (downstream join)** - a `vmi.attribution` observation record
  carries a per-model ModelBOM committed by the record's decoded `enrichment_digest`, whose entries
  follow the [ml-bom.md](ml-bom.md) raw-file-SHA-256 identity contract: an in-toto training-provenance
  attestation whose subject is the weight file keyed by the same raw-file hash chains to this evidence
  by digest equality, with no shared naming and no live environment.
- **in-toto `test-result`** - the fused-`result` precedent: a REQUIRED `result` enum recomputable from
  the carried lists. This predicate follows the same recomputable-summary discipline.
- **SVR / [SLSA VSA](https://slsa.dev/spec/v1.0/verification_summary)** - deliberately NOT this
  predicate (see Purpose). A consumer MAY emit an SVR/VSA downstream over this evidence, binding it by
  the evidence's DSSE payload digest; this predicate itself carries no delegated policy decision.
- **[DSSE](https://github.com/secure-systems-lab/dsse)** - the envelope, used at TWO layers: the outer
  statement envelope (see [signing-envelope.md](signing-envelope.md)) AND each `observationRecords[]`
  entry, which is itself a DSSE envelope over one observation payload (`payloadType`
  `application/vnd.probity.aee-observation.v1+json`). Reusing the standard shape keeps the inner
  records `cosign verify-blob`-compatible and gives every rail one verify path.
- **[SCITT](https://datatracker.ietf.org/wg/scitt/about/)** - a future transparency-log inclusion
  proof (a COSE **Receipt**) attaches at the DSSE **envelope** layer as a sibling to `signatures`,
  NOT in this predicate body; the envelope is the correct staple point and the predicate schema stays
  unchanged.

## Versioning

This is Adversarial Execution Evidence v0.7 (predicate type URI suffix `/v0.7`). Adding a new OPTIONAL
field - including additive `attackResults` optionals, `doesNotAssert` entries, and `_ext` extensions -
is additive and stays within the version; a verifier ignores fields it does not recognize, and an
unrecognized observation-record `aeeKind` covers nothing and is otherwise ignored (fail-closed forward
compatibility). A rename or removal of a field, a change to the DSSE observation-record shape or the
`+json` media-type rule, a change to the `containmentObserved` / `basis` / `method` / `posture`
vocabularies, a change to the run-binding construction (`aeeBindingVersion`), or a change to either
appraisal gate is breaking and requires a new version URI. A member is born exactly when a normative
reader consumes it: if a future version makes the shared-reference evidencing obligation checkable,
attribution strength acquires a normative reader at that version and becomes a required member then, not retroactively
and not through a verifier-invented heuristic in the meantime.

## Changelog

- **2026-07-29** - Run binding moved to `aeeBindingVersion: 2`, and the posture registry closed
  normatively. The pre-image gains `observationVocabulary`, the carried vocabulary digest, so
  narrowing the caught set after the run breaks every record's binding instead of re-deriving for
  free; and its `networkPosture` input becomes the RFC 8785 canonical digest of the carried
  `networkPosture` **object** rather than the value of that object's own `digest` member, bringing
  the posture string inside the signature it had been sitting beside. Both inputs are configuration
  already on the wire, so the change costs no bytes and adds no comparison - each closes through the
  equality every record's `aeeRunBinding` already runs - and both are fixed before corpus injection,
  which is the admission test for any binding input, since the arming record carries the digest and
  is signed before injection. Version 1 is retired with **no alias and no dual-accept window**; the
  absent-member default is now the implemented version rather than a fixed number, so omitting the
  optional declaration stays legal across a version change. Separately, `networkPosture.posture` is
  stated **closed** at its four registered values with an unregistered value malformed, resolving a
  divergence in which the upstream prose introduced the values as an example while the proto beside
  it, this document, the producer, both binding surfaces and the shipped admission policy all
  treated them as a closed, fail-closed set. New failure code: `posture-vocabulary`.
- **2026-07-29** - v0.6 timestamp profile and descriptor typing (spec text; no wire-shape change, no
  signed byte, digest, signature, or conformance vector moves). `issuedAt` is typed as the in-toto
  framework's `Timestamp` rather than as a bare RFC 3339 string, and the profile that type leaves open
  is pinned on the field: uppercase date-time separator and zone designator, and a zone designator of
  `Z`, `+00:00`, or `-00:00`. The zone rule was previously written only on `armedAt`, so a statement
  whose `issuedAt` carried `+05:00` was conformant here while being off-guideline, and the case rule
  was written nowhere; `armedAt` now cites the profile instead of restating half of it. The schema SSOT
  carried the field as `format: date-time`, which is annotation-only under the default draft 2020-12
  vocabulary and so asserted nothing; it now also carries a `pattern`, which every conforming validator
  enforces, so the schema states the profile rather than merely describing it.
  `observationEnvironment.substrate` and `observationEnvironment.catchPolicy` are stated to be
  ResourceDescriptors, `corpus` is stated NOT to be one, and the rule holding the remaining four
  members is written down: a member carrying the pre-image its own digest is taken over
  keeps that pre-image on the statement's own JSON surface, because the only descriptor member that
  could hold it is base64 `content` and material inside a base64 member sits outside every byte-level
  rule this document states. This profile narrows the descriptor rather than widening it - the schema
  SSOT's `additionalProperties: false` excludes `uri`, `downloadLocation`, `mediaType`, `annotations`,
  and `content` on both members.
- **2026-07-28** - v0.6 vetting rounds 7-9 (spec text; no wire-shape change). `aeeChainScope` is a
  duplicate-free array of registered dimension tokens (UTF-16 code-unit sorted) with a two-sided
  equality gate and fail-closed rejection of a non-array, an unregistered token, or a non-canonical
  order, replacing the free-form producer string that left scope-narrowing ungateable; the gap, fork,
  and genesis rules key on the evaluated tuple while consumer policy compares the declared dimension
  set. An arming payload MAY carry a read-first `aeeBindingVersion`, rejected fail-closed when
  unimplemented and never driving the derivation. The whole statement is parsed as strict I-JSON (a
  duplicate member anywhere is malformed) and every string literal MUST be a well-formed sequence of
  Unicode scalar values (valid UTF-8, no overlong form, no CESU-8, paired surrogate escapes, no raw
  control below U+0020, four-hex `\u`), checked on the raw bytes before any decoded string is read. A
  normative JSON nesting-depth bound of 128 is stated with its counting rule (open containers, the
  outermost brace is depth 1). `armedAt` requires a zero UTC offset, an out-of-range `observationRefs`
  index is a fault on any row, duplicate `attackId` rows are malformed, the single-subject requirement
  applies on a statement of any basis, and the three coverage sets are a disjoint partition. The
  fork-consistency ceiling gains its demand-disclosure yield (a consumer policy MAY require a
  contiguous, fork-free chain). No vector-forced reading is unpinned: agreement on a rule no vector
  exercises is untested rather than confirmed. Strict I-JSON also excludes the Unicode noncharacters
  (U+FDD0..U+FDEF, U+nFFFE/U+nFFFF) per RFC 7493 section 2.1, and the JSON nesting bound now charges a
  level per open container so an empty-container leaf is counted at its own depth.
- **2026-07-23** - v0.6 review revisions (spec text; no wire-shape change beyond optional additive
  members). BMP-only upgraded from producer hygiene to a verifier rejection obligation, with the
  UTF-16 sort rule retained as defense in depth. External anchors rescoped from verifier inputs to
  consumer-policy obligations with a single conjoined `admitted` admission result (validity AND
  tier policy AND anchors); the anchor comparison is not a validity gate. Run-entropy pre-image
  gains a SHOULD for a publicly datable, round-unpredictable component (proven signing-time floor;
  asserted ceiling unchanged). Optional `aeeRunSeq` / `aeePrevRunBinding` / `aeeChainScope`
  arming-payload members for cross-run gap evidence (ordering-only; equivocation semantics;
  registration-receipt completion, SCITT RFC 9943 / COSE receipts RFC 9942). Shared-reference
  evidencing restated as a producer obligation outside every gate; selectors do no covering work.
  Registered-claims precedent note (RFC 7519, RFC 9711, OCI, RFC 6839) with the fail-closed and
  verify-then-read departures stated.
- **v0.6** - Two-gate substrate backing for `basis: substrate`. Byte-checkable **coverage validity**
  (references resolve in range and class-match; every covering payload is canonical `+json` carrying
  the reserved members with `aeeRunBinding` equal to the derived run binding; `method` capped by the
  weakest signed `aeeMethod`; `batchRoot` recomputes) is a VALIDITY requirement and a consumption
  precondition (GATE 1, RATS "Appraisal Policy for Evidence") - a violation makes the attestation
  invalid, independent of any consumer. The one trust-relative step - the covering signatures verify
  against a consumer-named substrate key - is a per-row **evidence tier** `attested` / `unattested` /
  `declared` (GATE 2, RATS "Appraisal Policy for Attestation Results"); a consumer with no pinned
  substrate root treats every substrate row as `unattested`, and an `unattested` substrate row ranks
  with `artifact` (rank, never relabel). Caught intercepted rows are covered by `interception`
  records, reconstructed rows by `examination` records, and clean intercepted rows by BOTH a run-level
  `arming` record and a `sealed` record. The observation vocabulary now travels on-wire
  (`observationVocabulary`: labels, caught subset, JCS digest), so the recompute and GATE 1 are pure
  functions of carried bytes. Renamed `interceptRecords` -> `observationRecords`, `interceptRefs` ->
  `observationRefs`, and `does_not_assert` -> `doesNotAssert` (old spellings rejected, no alias).
  Record signatures are DSSE PAE over `(payloadType, payload)`; `payloadType` is now a producer-defined
  `+json` media type (Probity emits `application/vnd.probity.aee-observation.v1+json`). `batchRoot` is
  pinned to RFC 6962 with domain separation, **duplicate records rejected**, and the leaf is now over
  the record's PAE bytes; it is required whenever records exist. A new `runEntropy` digest folds a
  substrate-emitted run-start value into a versioned run binding, anti-splice
  rather than a freshness challenge. `issuedAt` is now REQUIRED. Removed the v0.3 payload chain,
  `expectedPayloadSha256`, `dstIp`/`dstPort`/`wasTls`, and the orphan-hmac degraded-record concept;
  attribution strength is non-normative producer nuance. Stated the field-partition (delta-I) and the
  named non-claims (run-population completeness out-of-wire; no cross-predicate composition).
- **v0.5** - Split the per-row `basis` into two orthogonal REQUIRED fields: `basis` (closed vocabulary
  `substrate` / `artifact`) names the vantage, and the new `method` (closed vocabulary `intercepted` /
  `reconstructed`) names the directness, both by weakest-input composition. The 0.4 values
  `substrate_observed` / `artifact_reported` / `inferred` are rejected, not aliased (`inferred` had no
  successor because it conflated the two axes). Made `actualLayer` REQUIRED on every row (a missing
  member is malformed, a different altitude than the fail-closed row members) and extended its literal
  `none` to caught rows (observed-but-not-enforced). Added the consumer strength orderings (`basis`
  bounds a `fail`, `method` bounds a `pass`, fail-closed rows at the lattice bottom) and stated the
  row-travel design invariant and the `basis`/`method` producer-claim trust boundary.
- **v0.4** - Added a required per-row `basis` field (closed vocabulary `substrate_observed` /
  `artifact_reported` / `inferred`, fail-closed on unknown) so each observation carries its own vantage;
  pinned the `actualLayer` clean-run behavior to the literal `none` rather than omitting the field; and
  renamed `does_not_assert` to `doesNotAssert` with no alias.
- **2026-07-14** - v0.3. Breaking change to the intercept-record shape (verify-then-read).
  `interceptRecords[]` entries became standard DSSE envelopes `{ payload, payloadType, signatures[] }`
  carrying the EXACT signed bytes, replacing the flat readable-field object; the readable fields moved
  INSIDE the signed payload and are read only AFTER the per-record signature verifies over
  `PAE(payloadType, b64decode(payload))`. `batchRoot` became a SINGLE predicate-level RFC-6962 field.
  Added `does_not_assert[]` and documented the orphan-hmac degraded state. (All superseded by v0.6.)
- **2026-07-08** - v0.2. Breaking restoration of the run-integrity anchors the v0.1 redesign dropped:
  the corpus `manifest.classes` became a `class -> attackId[]` map with coverage checked at ATTACK
  granularity; a run-identity binding on every intercept record (closes cross-run transplant);
  `interceptRefs` as an unforgeable foreign key; removal of the redundant `contained` boolean with
  `actualLayer <-> caught` coherence enforced; the composite payload chain; out-of-vocabulary
  `containmentObserved` fail-closed; CANON-EQ (signed payload must be its own JCS canonical form); and
  `expectedCorpusDigest` + `expectedSubstrateDigest` as REQUIRED offline-verifier inputs.
- **2026-07-06** - Initial v0.1. The greenfield evidence-first replacement for the retired
  `scorecard-verdict` predicate: fused recomputable `result` (replacing `verdict` + `verdict_reason`);
  `observationEnvironment` grouping the substrate / corpus / catchPolicy / networkPosture bindings; the
  digest-committed corpus `manifest` replacing a producer-asserted denominator; `attackResults` (was
  `cve_results`); `interceptRecords` (was `forensic_traces`); lowerCamelCase field names.
