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
predicateTypeURI, on the same footing and for the same reason as the sibling predicate inspec/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 ofvectors-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:
- Anchor
attestingKeyout of band and never from the record. - Run
rule_wellformedbefore reading any member for meaning. - Read a conforming record whose
attestationTierisvoluntaryas non-repudiable for what was signed and as evidence of no independently observed fact. - Treat
selfSignersas the producer's declaration, and compare it against its own key inventory rather than accepting it. - Decide explicitly what it does with a
producer_assertedfield, 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).