Proposals · SEP-3004 · Accepted

Tamper-Evident Audit Record Contract

Standards Track · Created 2026-06-02 · Source

Imported from upstream. Proposed as modelcontextprotocol#3004 by @scottrhodes on 2026-07-02, where it was still closed unmerged on 2026-09-25. Accepted into omcp on its technical merit. The text is the author's; only the status, the sponsor field, and documentation links were changed on import.

The text in force is the vendored specification, at extensions/audit-record/spec, not this page. This page is the 2026-07-02 proposal text: it names canonical_form_version without pinning a value. The pinned value is audit-record-contract/1, and the vendored specification is the normative statement of it.

Implemented in omcp 2026-09-27. The reference verifier, the specification text, and the conformance vectors are vendored at extensions/audit-record, with four rejecting vectors added for requirements that had accepting cases only. The reference chapter is published at Tamper-Evident Audit Records.

Reviewed 2026-09-25 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/notboatanchor/gif. The conformance vectors and the reference chapter that acceptance required are now in place.

It stays Accepted, not Final, deliberately. The normative text is the author's, and it is pinned here to an untagged commit. This moves to Final when @notboatanchor tags v1.0.0 upstream and the vendored copy is re-pinned to that tag — the author's call on when the text is settled, not ours.

Abstract

This SEP specifies an interoperability primitive: a canonical byte form and an append-only hash-chain construction that independent implementations produce identically and any third party can verify, with governance-specific context carried in registered extensions rather than in the core. What must agree across implementations is the bytes, not the semantics — so the construction is specified once and referenced by many, each layer's context riding as a registered extension on top.

The need is concrete: multiple MCP governance proposals require decisions to be recorded in a tamper-evident audit trail, but none defines what such a record is. PR #2809 makes a tamper-evident admission record a SHOULD without defining the record's shape or a shared, verifiable form for it; PR #2624 states as a compliance consideration that audit trails "MUST" be tamper-proof, with no checkable guarantee; caller-governance layers emit incompatible shapes. The guarantee is named at every seam and given a shared definition at none.

This SEP defines one vendor-neutral audit record contract: a minimal protected core; a type-keyed extensions mechanism so each layer attaches context under one integrity construction; deterministic sorted-JSON canonicalization aligned with PR #2809; an append-only hash chain; a verification procedure; and a structured attestation manifest for the part that is not protocol-observable.

Motivation

Audit is the one governance guarantee every layer asserts and none defines. Across three independent proposals:

  1. Admission (PR #2809). A conforming host SHOULD append a tamper-evident record for every admission decision. The proposal specifies the admission decision and its trust model; it does not define the record's shape or a shared, cross-layer verifiable form for the tamper-evident guarantee — the gap this contract fills.
  2. Interception / runtime security (PR #2624). Compliance Considerations require that "audit trails are tamper-proof" — a deployer obligation, not a checked property; the proposal's own audit mechanism is non-blocking observability.
  3. Caller governance. Session- and persona-scoped governance layers emit per-action decision records, but in shapes that vary by implementation (field names, what is integrity-protected, whether the record is append-only at all).

These three emit differently shaped records — caller-governance carries a declared purpose and delegating principal; runtime-security carries detections, drift status, and data classes; admission carries the clearance decision. No single layer's record shape is the universal one. What they share is not a field set but a construction: an append-only, canonically-hashed, chained, independently-verifiable record. Specifying that construction once — with a minimal common core and a typed extension point for each layer's context — lets all three conformance-anchor to one definition without flattening their differences. SEP-2484 makes a conformance check — or a documented exclusion — the bar for Final; today the tamper-evident guarantee has no shared definition to write a check against at any layer. Specifying the record and its verification once gives every layer one definition to reference and one vector set to anchor to, instead of each re-deriving and re-testing "tamper-evident" alone.

This SEP specifies that construction once. It does not move the guarantee into any one layer; it defines an orthogonal record contract the other proposals reference by forward-reference today and upgrade from SHOULD to MUST when this SEP lands.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

This contract constrains an audit record and the record chain it belongs to. It does not constrain the storage technology, the wire protocol used to read records, or which governance layer authors them. Any implementation that produces records satisfying §2.1–§2.7 and §2.9 conforms (§2.8 is reserved and out of scope for v1), regardless of mechanism.

2.1 The Audit Record — Protected Core

An audit record is a structured object describing one governed event. Every conforming record, whatever extensions it carries, MUST contain the following core fields, all of which are integrity-protected (§2.4):

Field Type Requirement
event_id opaque unique id MUST — unique within the chain
occurred_at RFC 3339 timestamp (UTC, Z, millisecond precision — §2.3) MUST — assigned by the recorder's clock, MUST NOT be settable by the governed caller
principal_id opaque id MUST — the governed identity the event is attributed to
event_type string from a registered vocabulary MUST
tool_name string | null MUST be present; null where no target applies
outcome allowed | denied | deferred | error MUST — the abstract disposition of the event (§2.1.1)
extensions object keyed by registered type id MUST — at least one registered extension (§2.2); at most one entry per type id
previous_hash hash digest | null MUST — links to the prior record (§2.4); null only for the first record in a chain segment
event_hash hash digest MUST — the integrity digest of this record (§2.4)

The core is deliberately emitter-neutral: identity, time, principal, the acted-on target, the disposition, and the chain linkage. Layer-specific accountability context does not belong in the core; it belongs in the extensions object under a registered type id (§2.2). Implementations MUST ignore unknown top-level fields rather than rejecting a record, to permit forward extension.

2.1.1 The outcome disposition enum

outcome is a closed, abstract disposition vocabulary, not a per-layer status code. The cross-profile anchor is the terminal/non-terminal axis:

Domain-specific dispositions, reason codes, and severities MUST NOT extend this enum; they live inside the emitting layer's extension data, bound to the base outcome (an extension supplies decision evidence that informs the core outcome, never a competing outcome — e.g. a quarantine decision that holds the event maps to deferred, a terminal quarantine maps to denied). The classification of an event_type or extension field as security-relevant is registration prose, not a record field — keeping it out of the digest.

2.2 Registered Extensions

The record contract is extended only through registered extensions, never through new chain constructions. A record carries an extensions object — a JSON object keyed by registered type id, each key's value being that extension's typed data: extensions: {"<type>": <data>, ...}. Extension data is canonicalized into the same preimage as the core (§2.3) and is therefore integrity-protected by the same chain — there is exactly one chain construction, shared across all extensions, and the type id is itself a preimage key, so an extension's type cannot be swapped without breaking verification.

Structural rules:

An extension registration defines: the type id; its REQUIRED and OPTIONAL data fields and their types (and any closed value vocabularies); the event_type vocabulary it recognizes (§2.9); and how its decision evidence binds to the core outcome (§2.1.1). A record's data for each declared extension MUST satisfy that registration's required fields.

This SEP registers and fully specifies one extension, records the converged shape of a second whose normative registration text is contributed by its implementer, and names a third contributed by reference:

New extensions are added by registration without altering §2.1, §2.3, or §2.4. An extension MUST NOT redefine a core field or introduce a second canonicalization.

2.3 Canonicalization

Integrity (§2.4) is computed over a deterministic canonical form of the record's protected fields (the core minus event_hash, plus the full extensions object). The canonical form is sorted-key canonical JSON, aligned with the canonicalization used by PR #2809's clearance assertion so that adopters process clearance assertions and audit records on one code path and one vector matrix. A canonicalization MUST:

The canonical form is identical across extensions; only the data each type id keys differs. A fixed two-extension known-answer test — one record carrying both caller-governance and runtime-security, reproducible from a shell with sha256sum — pins how two extensions hash side by side. (The fixed value, with its canonical preimage, is given under Conformance; the companion vectors reproduce it and the full matrix programmatically.)

2.4 Integrity Construction (Record Chain)

Records form an append-only hash chain. For each record:

H MUST be SHA-256 at baseline. An implementation MAY use a stronger function; the function in use MUST be recorded in the attestation manifest (§2.7) so a verifier is unambiguous (algorithm agility). Implementations MUST NOT use a hash function with a known practical collision. The digest values carried in the record (event_hash, previous_hash) MUST be expressed as lowercase hexadecimal: digest comparison (§2.6) is then exact string equality, and a digest re-entering the preimage as the next record's previous_hash has a single byte form across implementations.

A chain "segment" is a contiguous run of records over which previous_hash threading is continuous. Implementations MAY segment chains (e.g. per time window or partition); each segment MUST be independently verifiable and the segmentation boundary MUST be discoverable to a verifier.

2.5 Append-Only Requirement

Once committed to the chain, a record MUST NOT be modified or deleted by any caller — including a privileged operator — without that alteration being detectable by the verification procedure (§2.6). Concretely:

The enforcement mechanism is implementation-defined and is explicitly NOT constrained by this SEP; it is declared in the attestation manifest (§2.7). Conforming mechanisms include, and are not limited to: storage-engine permission revocation plus row-level policy denying mutation; write-once media; an append-only log service; or a ledger.

2.6 Verification Procedure

A conforming chain MUST be verifiable by an independent party in possession of the records, by the following procedure:

  1. For each record, recompute H( canonical_form( protected_body ) ) and compare it to the stored event_hash. A mismatch indicates the record was altered after commitment (in any protected field, core or extension).
  2. For each record after the first in a segment, confirm previous_hash equals the prior record's event_hash. A mismatch indicates insertion, deletion, or reordering.

The procedure runs identically whether executed by an adopter attesting to its own trail or by a third party with read access to an exported record set. It MUST be deterministic: the same record set MUST always produce the same verdict.

2.7 Attestation Manifest (Structured Attestation)

The append-only enforcement of §2.5 is a storage property, not a wire-observable one — so gating it at the protocol surface would be theater. It is therefore attested, but attestation MUST be structured: a bare "trust us" claim is not conformant. A conforming implementation MUST publish a machine-readable attestation manifest declaring:

The machine-checkable vectors (the companion vector set) verify the observable record (schema, extension validity, canonical form, chain threading, emission). The manifest plus the referenced verifier make the integrity construction attested-but-auditable rather than attested-in-the-abstract: a reviewer can re-run the declared verification over the records and reproduce the verdict.

2.8 External Anchoring (Reserved; Out of Scope for v1)

Anchoring a chain to an external witness (e.g. RFC 3161 timestamp, version-control commit, object-store checksum, public ledger) addresses a distinct, weaker-priority threat — an auditing organization rewriting its own history — that is orthogonal to the within-system chain construction. Selecting a witness scheme is deferred to avoid binding v1 to an ecosystem pick.

A field name anchor_witness is reserved for this purpose. In v1 it MUST be absent or null and is NOT included in the integrity preimage (§2.3); a follow-on anchoring extension defines its semantics and whether it enters the preimage. Anchoring's absence MUST NOT affect conformance to §2.1–§2.7.

2.9 Emission Completeness

This SEP defines the record and its construction, not the policy of which events a given governance layer must record — that belongs to each referencing proposal / extension registration. A referencing proposal that adopts this contract:

Rationale

Why a separate record contract, rather than audit inside each layer. Audit is asserted at several seams (admission, interception/runtime-security, caller-governance) and given a shared, verifiable definition at none; each names the guarantee without defining a record shape the others can verify against. What those layers share is not a field set but a construction — an append-only, canonically-hashed, chained, independently verifiable record. Specifying that construction once, with a minimal common core and a typed extension point per layer, lets all of them conformance-anchor to one definition without flattening their differences. This follows the project's convergence principle (one well-defined way to solve a problem) rather than fragmenting audit across N incompatible shapes.

Key design decisions and alternatives considered.

Evidence of community process. The contract was developed in the MCP Security Interest Group (#security-ig) with the author of the admission proposal (PR #2809) and the runtime-security implementer, Syed Maaz Ahmed (Interlock; cross-ref PR #2624). The runtime-security extension's normative registration text was contributed by Maaz rather than authored top-down. The two-extension known-answer digest reproduces byte-for-byte from the published rule plus stock sha256sum across independent implementations — the cross-implementation agreement the construction is meant to produce.

On the "make it an extension" question. A reasonable reading of the project's composability and standardization principles is "build audit on top, as an extension, rather than in the specification." This contract is positioned as an interoperability primitive — a canonical byte form that independent implementations must agree on for cross-vendor verification — not as a protocol feature or as governance-in-core. The governance semantics ride in registered extensions, off the core; the only thing standardized is the construction multiple layers already converge on. Positioned this way, the record contract is the cross-cutting evidence layer beneath the other proposals: admission (#2809) decides whether a server may be used, interception/runtime-security (#2624) decides whether an admitted surface drifted, and caller-governance decides whether a caller may invoke — three different decisions, all three needing the same tamper-evident record to be accountable. Standardizing that record once is what lets the three compose without each re-inventing "tamper-evident"; it is the interop seam between them, not a governance feature competing with any of them.

Backward Compatibility

This SEP is purely additive and introduces no backward incompatibilities.

Conformance

Conformance is split along what is protocol-observable, consistent with SEP-2484's documented-exclusion model.

Machine-checkable (vectors provided). Given a set of records and a manifest:

A normative, runnable vector set for C-REC-1…7 is published with the reference implementation, in the GIF repository under mcp-server/conformance/audit-record-contract/ (https://github.com/notboatanchor/gif/tree/e1f02a95506e81e7766c3ba3a684ecad7cfff12f/mcp-server/conformance/audit-record-contract). From mcp-server/, npm run vectors yields 23 vectors — 23 passed, 0 failed (zero-dependency alternative: node --experimental-strip-types conformance/audit-record-contract/run.ts). Because the integrity guarantee is off-wire (§2.7), these are record/verifier vectors evaluated over an exported record set — not wire-protocol scenarios — providing the machine-checkable evidence SEP-2484 requires for Final, paired with the structured attestation (§2.7) for the part that is not protocol-observable.

Two-extension known-answer test (self-contained). The cross-implementation interop anchor referenced in C-REC-3 is fixed here so it can be reproduced with no repository and no dependence on any implementation — a single segment-head record (previous_hash: null) carrying both registered extensions, pinning how two extensions hash side by side under one digest. The identifiers are vendor-neutral constants; event_hash is the computed output and is excluded from the preimage.

field value
event_id 99999999-9999-9999-9999-999999999999
event_type tool_call
occurred_at 2026-06-06T12:00:00.000Z
outcome deferred
previous_hash null
principal_id aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
tool_name export
extensions › caller-governance flagged=false, invoked_by_principal_id=null, purpose_declared="reconcile June invoices", session_id="55555555-5555-5555-5555-555555555555"
extensions › runtime-security drift_status="confirmed", evidence_hash="sha256:b2c547e2…e2d9be", policy_id="example.org/runtime-drift@3", quarantine_decision="quarantine", severity="high"

The canonical form (§2.3) of the protected body is the exact byte string:

{"event_id":"99999999-9999-9999-9999-999999999999","event_type":"tool_call","extensions":{"caller-governance":{"flagged":false,"invoked_by_principal_id":null,"purpose_declared":"reconcile June invoices","session_id":"55555555-5555-5555-5555-555555555555"},"runtime-security":{"drift_status":"confirmed","evidence_hash":"sha256:b2c547e2c8f17eafc72ef5c2d4d7b6b4d0f7437ab52bae573a9af14ff5e2d9be","policy_id":"example.org/runtime-drift@3","quarantine_decision":"quarantine","severity":"high"}},"occurred_at":"2026-06-06T12:00:00.000Z","outcome":"deferred","previous_hash":null,"principal_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","tool_name":"export"}

whose SHA-256 is:

f733fed9cc757165f810b778e4baba1f51a45504988e937707aaab4361b2f064

Reproduce from any shell — paste the preimage block above between the quotes:

printf '%s' '<canonical preimage above>' | sha256sum
# -> f733fed9cc757165f810b778e4baba1f51a45504988e937707aaab4361b2f064

Because the digest is a property of the canonical bytes alone, any implementation adopting §2.3 lands on the same value. The published vector set additionally pins the single-extension caller-governance digest (d494769c…) and the full C-REC-1…7 matrix.

Attested-but-structured (not protocol-observable). The append-only enforcement of §2.5 is attested via §2.7. A conformance harness verifies the consequence (chain verification fails after an out-of-band mutation, C-REC-4) and the structure of the attestation (C-REC-7), rather than the storage mechanism itself.

Open Questions

(The design decisions resolved during #security-ig review — extension representation, canonical form, the outcome vocabulary, value types, and the attested-but-structured verification surface — are recorded in the Rationale's "Key design decisions and alternatives considered.")

Security Implications

A hash chain provides tamper-evidence, not tamper-prevention: it guarantees that alteration of committed records is detectable, not impossible. Detection depends on at least one trustworthy copy of a prior event_hash — the motivation for the reserved external-anchor hook (§2.8) against adversaries who can reach the record store. Because purpose_declared and other extension fields are inside the chain (§2.2), the privileged-actor-rewrites-intent vector is closed: altering a recorded reason breaks verification. The integrity guarantee is only as strong as the protected field set; fields an extension registration chooses to leave out of its data object are unprotected. Timestamps are recorder-assigned (§2.1) to prevent a governed caller from backdating records. String normalization (§2.3) bounds — but does not eliminate — the risk of homoglyph/encoding ambiguity in free-text fields; it does not address confidentiality of record contents, which may carry sensitive governance context and SHOULD be access-controlled independently of integrity protection. A verifier processing records from an untrusted source SHOULD bound canonicalization (§2.3) recursion depth: deeply-nested input is otherwise a stack-exhaustion vector, and conforming records are shallow, so a generous cap rejects only hostile input (the reference verifier caps depth accordingly).

Reference Implementation

An open-source reference implementation of this construction exists: the Governed Intelligence Framework (Apache 2.0, https://github.com/notboatanchor/gif). It implements the core + chain construction + the caller-governance context. Its canonical form matches this contract's rule (sorted-JSON, millisecond RFC 3339 timestamps, purpose_declared in the preimage). It implements this draft's extensions keyed-object shape (canon_version = gif-audit/2, migration 015; the predecessor single-profile shape gif-audit/1 merged in gif PR #28). Its audit trigger emits and reproduces the single-extension caller-governance known-answer digest (d494769c…) as its own self-test; the two-extension digest (f733fed9…, given under Conformance) is reproduced by the same canonical rule over the published preimage — and exercised synthetically in the conformance vectors — rather than minted by the live trigger. This is consistent with the digest being a property of the canonical bytes, not of any one implementation: a second implementer (the runtime-security implementation, cross-ref PR #2624) independently reproduced it from the rule alone. It produces caller-bound, append-only, SHA-256 hash-chained audit records; enforces append-only at the storage layer via privilege revocation plus row-level policy and a trusted hashing trigger; and ships runnable conformance scenarios for record emission, append-only behavior, and scope-gated read. Per the spec-vs-source discipline, the reference implementation does strictly more than this contract requires (it binds records to a persona/session governance model and a scope system not specified here), and the contract is written to admit implementation paths other than the reference one (storage engine, enforcement mechanism, and every extension beyond caller-governance are open).

Independent implementations of the same construction under other extensions exist — a runtime-security receipt (cross-ref PR #2624) and an admission record (cross-ref PR #2809) — which is the intended outcome: the construction is the invariant, the extension is the variation, and multiple implementation paths anchor to one definition.

Acknowledgments

This contract was developed in the open in the MCP Security Interest Group (#security-ig). The admission-control extension is named by reference to the Attested Tool-Server Admission proposal (ATSA, PR #2809).

References