agent-evidence-vectors v0.37.0

Self-reported agent record, v1

Type URI: https://probityai.github.io/agent-evidence-vectors/predicate/v1/self-reported-record

Version: 1.0.0

Status: DRAFT, issued under a self-hosted predicateType URI, on the same footing and for the same reason as the sibling predicate in spec/predicates/observed-effect.md. The URI is an identifier and not a retrieval address; this document is the authority. The URI is stable and will not move, because every member of vectors-self-reported-record/ carries it inside bytes a signature covers.

Purpose

An agent runtime that keeps a record of what its agent did is keeping a record of itself. This contract is the shape such a record has to take before a distrusting reader can get anything out of it, and the shape is almost entirely negative: it says which of the record's claims the record must make checkable, and it refuses a record that presents an assertion where an observation would have to be.

The sibling predicate in spec/predicates/observed-effect.md is the did half: a mutation interval named from a vantage the observed party does not control. This contract is the half that exists because most deployed agent runtimes have no such vantage and ship a record anyway. It does not make a self-reported record into an observed one. Nothing can. What it does is make the five specific substitutions enumerated under Attacks this contract refuses refusable from the record's own bytes, so that a reader who declines to trust the runtime is refusing a record rather than guessing about one.

Where the terms come from

Five members of this record are values of terms registered in agent-evidence-vocabulary, spelled in this document's lowerCamelCase because in-toto predicate members are spelled that way and the registry's own crosswalk rule permits the re-spelling where it is declared. The registry is the definition; this contract is a consumer of it.

member registry term registered values
originKind evidence_dimensions.origin_kind self, third-party-control-plane, log-import
attestationTier evidence_dimensions.attestation_tier voluntary, authoritative
observationVantage evidence_dimensions.observation_vantage substrate, artifact
observationDirectness evidence_dimensions.observation_directness intercepted, reconstructed
fields[].evidence evidence_dimensions.field_evidence_partition substrate_covered, producer_asserted

The registry defines those axes. It does not define a refusal code, and this contract does not pretend it does: the five codes under Rules are this contract's own, in the same sense vectors/MANIFEST.json already states for its own code set, and each one is named after the registry value whose claim it refuses. A rail naming a different code for the same member has produced a reason-parity datum, not a failure.

Schema

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "<sessionId>", "digest": { "sha256": "<64-hex>" } }
  ],
  "predicateType": "https://probityai.github.io/agent-evidence-vectors/predicate/v1/self-reported-record",
  "predicate": {
    "recordId": "<producer-scoped, stable, opaque>",
    "sessionId": "<the session this record is about>",
    "hashAlgorithm": "sha256",
    "originKind": "self",
    "attestationTier": "voluntary",
    "observationVantage": "artifact",
    "observationDirectness": "reconstructed",
    "attestingKey": {
      "keyid": "<64-hex, sha256 over the raw public key>",
      "publicKey": "<64-hex, the raw 32-byte Ed25519 public key>"
    },
    "selfSigners": ["<64-hex keyid the observed runtime holds or can mint>"],
    "turns": [
      {
        "turnId": "<opaque>",
        "goalMarker": "<the turn's own statement of what it was for>",
        "priorTurn": "<turnId>",
        "changeIds": ["<changeId>"],
        "attestedBy": { "keyid": "<64-hex>", "sig": "<base64 over the turn preimage>" }
      }
    ],
    "memoryReads": [
      {
        "path": "/srv/agent/.vault/memory/trial-lesson.md",
        "assertedDigest": "<64-hex the record's own verify surface reports>",
        "pathDigest": "<64-hex of the bytes at path at read time>"
      }
    ],
    "ledgers": [
      { "name": "session", "members": ["<changeId>"] },
      { "name": "change-log", "members": ["<changeId>"] }
    ],
    "fields": [
      {
        "name": "provenance.model",
        "value": "<string, always a string>",
        "evidence": "producer_asserted"
      }
    ],
    "doesNotAssert": ["<explicit negative-scope statements>"],
    "issuedAt": "2026-09-20T00:00:05Z"
  }
}

Every member listed is REQUIRED. turns, memoryReads, fields and doesNotAssert MAY be empty; ledgers MUST carry at least two entries, because one ledger cannot disagree with anything and a record carrying one is a record that cannot be caught by srr-c-4. selfSigners MAY be empty, and an empty set is the claim that the producer knows of no key the observed runtime holds.

Nothing has a default. A missing member, or a value outside a closed vocabulary, makes the record malformed, and a verifier MUST NOT supply either: a verifier-invented default is a rule the record did not carry.

Preimages

Two signatures appear in this contract and neither is over the whole record. Each covers a named subset, canonicalized under RFC 8785, with a context string inside the preimage so that no byte string valid under one is valid under the other.

A turn's attestedBy.sig covers the canonical bytes of

{
  "changeIds": <the turn's changeIds>,
  "context": "agent-evidence/self-reported-record/v1/turn",
  "goalMarker": <the turn's goalMarker>,
  "priorTurn": <the turn's priorTurn>,
  "sessionId": <the record's sessionId>,
  "turnId": <the turn's turnId>
}

A field's coveredBy.sig, where present, covers the canonical bytes of

{
  "context": "agent-evidence/self-reported-record/v1/field",
  "name": <the field's name>,
  "recordId": <the record's recordId>,
  "value": <the field's value>
}

sessionId is inside the turn preimage so that a turn signed for one session is not replayable into another. recordId is inside the field preimage for the same reason, one level down.

Rules

Five rules, each a function of the carried bytes alone, plus the well-formedness precondition. No rule reads the network and none reads a trust anchor: the attestingKey.publicKey travels with the record. Carrying the key proves nothing about who holds it, and a consumer that needs the attesting identity to be true anchors it out of band exactly as it would anchor any signer. What carrying it buys is that every rule below is checkable offline by a stranger.

srr-c-1 — a turn is signed by the key the record names

rule_turn_attestation. Every member of turns MUST carry attestedBy, and attestedBy.sig MUST verify under attestingKey.publicKey over that turn's preimage. A turn whose signature does not verify is refused with turn-unsigned-by-attesting-key.

The rule exists because a turn is the unit a reader takes as history. A record that carries a turn nothing signed is carrying the producer's assertion in the shape of a ledger entry, and field_evidence_partition has a word for that (producer_asserted) which a turn has no place to write.

srr-c-2 — a memory digest is the digest of the bytes at the path

rule_memory_digest_over_named_path. Every member of memoryReads MUST carry assertedDigest equal to pathDigest. A member where they differ is refused with memory-digest-not-over-named-path.

The two members exist separately because they come from two places. A runtime that keeps a signed copy of a memory and also names the file it came from is holding two digests and reporting one, and the one it reports is the one nothing downstream reads.

srr-c-3 — the attesting key is not one the observed runtime can mint

rule_attesting_key_disjoint. attestingKey.keyid MUST NOT appear in selfSigners. A record where it does is refused with attesting-key-in-self-signers.

This is the same discriminator the sibling predicate applies to its prior commitment, and the two are deliberately identical: a signature made by a key the observed party can generate at will verifies perfectly and establishes nothing. The rule is not that the signature is bad. The rule is that the record's own declaration of which keys the runtime holds contradicts its claim to have been attested by something else.

srr-c-4 — every ledger the record carries names the same change set

rule_ledgers_agree. Every member of ledgers MUST name the same set of changeIds. A record whose ledgers disagree is refused with ledger-members-disagree.

A runtime that maintains a change log and a session ledger maintains two answers to one question. Where a removal is applied to one and not the other, both surfaces stay internally consistent and each looks authoritative, and no read either of them offers reconciles them. This rule is the reconciliation, done where it is cheap: inside the record, by set comparison.

srr-c-5 — a field claiming coverage carries the covering signature

rule_field_coverage. A member of fields declaring evidence: "substrate_covered" MUST carry coveredBy, whose sig verifies under coveredBy.publicKey over that field's preimage and whose keyid MUST NOT appear in selfSigners. A field declaring producer_asserted MUST NOT carry coveredBy. A violation is refused with field-claims-coverage-uncovered.

This is the rule the whole contract is built around. A record may carry any number of values it received from its caller, and carrying them is not a defect: a caller-supplied model name declared producer_asserted is an honest record, and the accept twin in the corpus is exactly that record. What is refused is the same value declared as covered by an observation, with nothing carrying the observation.

Well-formedness

rule_wellformed. Required members present, subject a single entry with a sha256 digest, every closed vocabulary value registered, every digest 64 lowercase hex, issuedAt an RFC 3339 UTC timestamp, ledgers of length two or more, every fields[].value a string. A record failing any of these is record-malformed, and malformed is a distinct verdict from invalid: it is reached before any rule above reads a field for meaning.

fields[].value is a string unconditionally, including where the underlying value was an integer, so that two rails cannot re-serialize one number two ways and disagree on a preimage they both computed correctly.

What a self-reporting party can still do

Held against the bar honestly, member by member. A contract whose author reports no residual did not look.

Refused by the rules above. A turn the attesting key did not sign. A memory digest that is not the digest of the bytes at the path the record names. An attesting key the record's own declaration puts inside the runtime's reach. Two ledgers that disagree. A field claiming an observation nothing covers.

Not refused, and the contract says so.

S1. Every value is chosen by whoever builds the record. pathDigest is asserted to be the digest of the bytes at path and nothing inside the statement establishes it. A producer that computes both digests from one source satisfies srr-c-2 trivially. What the rule buys is narrower and real: a producer that computes them honestly from two sources cannot report one and omit the other, and a record whose two sources disagree becomes refusable instead of silently green.

S2. selfSigners is the producer's own declaration. srr-c-3 compares the attesting key against a set the producer wrote, so a producer that omits a key from selfSigners passes. The comparison is worth making anyway, for the reason the sibling predicate gives for the identical check: the omission has to be decided in advance, it is a false statement inside bytes a key signed, and it is detectable outside the record by a key inventory. Neither is detectable inside it.

S3. A row that was never written is not detectable. turns, memoryReads and fields are each required and each may be empty, and no function of the carried bytes finds a row a producer declined to carry. This is the same class as the sibling predicates' read-set omission and the reasoning is unchanged: a record that withdraws a claim is a record an honest producer with weaker instruments emits from the same configuration, so no rule refuses the one without refusing the other. doesNotAssert exists so a known gap travels. It is not a defence against a producer that lies about having one.

S4. The subject digest is not recomputed. The subject entry names the session and a digest, and this contract checks the digest's shape and never its derivation. A producer chooses it. It is carried because the in-toto framework requires a subject, and it is named here as unchecked rather than left to look checked.

S5. Nothing here signs the record as a whole. The two signatures cover named subsets. Envelope signing, transparency and receipt carriage are a separate layer, and this repository's profiles/scitt-cose.md and vectors-scitt-cose/ are that layer. A consumer that needs the whole record non-repudiable wraps it there.

Stated as the rule a consumer should apply: originKind: self with observationVantage: artifact and attestationTier: voluntary is the honest spelling of a self-reported record, and a conforming record of that shape is still a self-report. The registry's own prohibition governs what may be done with it: a verifier MUST NOT read a voluntary attestation as evidence that the attested content corresponds to any independently observed fact.

Attacks this contract refuses

Each of the five was run against a deployed self-reported agent record, and each succeeded there: the record was accepted, or the tool reported it verified, with nothing refusing. They are listed as the record each attack would have to produce under this contract, and the rule that refuses it.

A1. A forged turn chained into an honest session. A transcript no agent produced, submitted through the same interface the honest turns used, against a live session identifier. The store appended it, chained it to the last honest turn, and a later read rendered it as history. Under this contract the forger has to produce a turn whose attestedBy.sig verifies under the session's attesting key, and it does not. Refused by srr-c-1.

A2. A memory inverted on disk while the verify surface reads a stored copy. The bytes of a memory file were replaced on disk, with a security instruction inverted to its opposite. The tool's verify surface read a signed copy held elsewhere, reported the same digest as before and the same verified verdict, while a fresh reader of the named path ingested the inverted text. Under this contract the record carries both digests and they differ. Refused by srr-c-2.

A3. Re-attestation under a second self-minted key. Once the tamper was synced, the attestation went stale and the tool said so. A second local identity was then created and the inverted content re-attested under it; verification against that identity returned verified, correctly, because the signature was valid. There was no notion of an authorised signer. Under this contract the second key is one the runtime minted and the record's own selfSigners says so. Refused by srr-c-3.

A4. An unrecord that leaves one ledger consistent and the other stale. A recorded change was removed. The change log re-folded and rendered a shorter, internally consistent history; the session ledger still named the removed change as a live turn; the index-rebuild command was additive and never pruned it. No read reconciled the two. Under this contract both ledgers travel inside one record and their member sets differ. Refused by srr-c-4.

A5. Twelve provenance fields echoed from the caller, with no signature over any of them. A single call carried a fabricated model name, fabricated token counts, a fabricated cost, a fabricated step count, a fabricated reasoning transcript and a fabricated reasoning signature. Twelve of them were persisted as the turn's provenance, nine as exact echoes and three renamed or re-encoded, and a search of the persisted records found no cryptographic signature over any turn record at all — the only field whose name contained "signature" was the caller-supplied string itself. Under this contract those twelve values are carryable, and declaring them producer_asserted is an accept. Declaring them substrate_covered with nothing covering them is refused by srr-c-5.

A6 is not listed, because there is no sixth attack. The corpus carries a sixth reject member that exists to force rule_wellformed and nothing else, and the corpus README says so rather than presenting it as a finding.

Consumer policy obligations

A consumer implementing this contract MUST, at minimum:

  1. Anchor attestingKey out of band and never from the record.
  2. Run rule_wellformed before reading any member for meaning.
  3. Read a conforming record whose attestationTier is voluntary as non-repudiable for what was signed and as evidence of no independently observed fact.
  4. Treat selfSigners as the producer's declaration, and compare it against its own key inventory rather than accepting it.
  5. Decide explicitly what it does with a producer_asserted field, and never read one as an observation because it arrived inside a signed record.

Changelog

1.0.0 is the first published version. Two members are named now as candidates for becoming required later, so the commitment is on the record rather than invented afterwards: a priorCommitment of the shape the sibling predicate defines, which is what would give this record's timestamps any standing; and a recompute rule for the subject digest, which is what would close S4.

Record format: draft-gilda-audit-agent-audit-record-03, Section 4 (an in-toto Statement carried in a DSSE envelope).