SEP-3004: Tamper-Evident Audit Record Contract - #3004
Conversation
This comment was marked as spam.
This comment was marked as spam.
|
Thanks — the SMF/WORM lineage is a fair parallel. Append-only records with continuity verification predate this ecosystem by decades; this contract standardizes that construction at the MCP seam, so prior art of that shape is useful context. On the offers: the admission-control registration text is intentionally left with the admission proposal. Open Question A records that choice, so its semantics stay with ATSA (#2809), whose author is a co-author here. If you want to develop that mapping, #2809 is the right venue and I'd take the offer there. §2.8 is reserved for a follow-on by design; anchoring practice will be relevant when that opens. AI assistance disclosure: drafted with AI assistance (Claude Code), consistent with the disclosure in the PR description. |
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
|
Thanks, that boundary makes sense. The way I read SEP-3004, its strength is precisely that it standardizes the record construction while leaving domain semantics with the proposals that own them. From the MCP-RE side, the complementary seam would therefore not be a change to the core, but a possible future registered extension that commits to independently verifiable runtime evidence—for example, an evidence profile identifier, digest, and optional reference. That would let SEP-3004 answer whether the audit history is internally intact, while the referenced evidence profile answers whether the recorded event is cryptographically bound to the actual MCP request, response, signer, and authority context. No need to combine the specifications, but it may be useful to keep that composition point in mind for a follow-on registration. |
|
That's the layering the contract is built for. The record core stays agnostic about what any given evidence proves; a registered extension commits to its own guarantee under its own profile identifier. The admission-control registration in #2809 already registers this way, so a runtime-evidence registration would be the second worked example rather than a new pattern. When MCP-RE is ready for that follow-on, section 2.2 defines the registration surface. Happy to look at a mapping whenever it's useful. |
|
Independent reproduction, since the thread says that half needs no coordination: I Two things fell out that may be worth something to the SEP itself. A three-extension known-answer vector.
One thing the core structurally cannot carry. §2.1 makes @matssun — the decomposition you sketched (evidence profile identifier, digest, optional Caveats, plainly: the acts come from a small self-hosted testnet, not a production network, AI assistance disclosure: built and verified with AI assistance (Claude Code), consistent |
|
Verified on my side as well: 81aea291 recomputes from the printed preimage under the section 2.3 rule, and your delta claim holds, the only change from f733fed9 is the third extension body. An implementation written from the spec text alone landing on the published digests is exactly the property the contract exists to pin. Thank you for running it and for publishing the bytes. The three-extension vector is worth pinning when the vector set next revs. C-REC-6 already claims emitter-neutrality, and a third registered type exercises it rather than restates it, as you say. Your conformance direction is the right one: recomputability, not agreement. That principle is what separates a vector set from an attestation, and I would keep it stated that plainly. On occurred_at, that boundary is deliberate. The core records only what the host can attest at its own trust boundary, and the host cannot attest the caller's clock, so caller-side signed time belongs in an evidence extension. You are right that such an extension earns its place by being able to disagree with the record it rides in. The registration surface in section 2.2 was written for exactly that kind of guarantee, and two independent arrivals at the same profile-id, digest, reference shape says the registration cut is sound. It stays MCP-RE's to land. |
|
Conformance feedback from a small independent deployment (~3 weeks live, four weekly anchor batches, 16 append-only lines). I run an append-only JSONL hash chain with a per-line SHA-256 sidecar, published here:
First commit 2026-07-18; current chain spans ISO weeks W29–W32. Canonicalization boundary. Verification hashes the raw JSONL line bytes (including the trailing newline). Row serialization uses compact JSON ( Append granularity. Weekly batches append multiple records per ISO week (one row per anchored asset). Validators verify line-by-line against the sidecar, not “one digest per calendar week.” Unclear whether verifiers should treat a week as an atomic unit or accept partial-week appends. Digest maintenance cost. Each append rewrites the full sidecar with SHA-256 of every line (currently 16; grows linearly). Cheap at this scale, but the spec does not say whether incremental or Merkle-style updates are expected — that affects how third parties budget verification on long chains. I have not experienced a chain break in production yet, so I have no operational recovery story to offer. Question: for multi-record weekly appends, should conformance tests require verifiers to accept any prefix of a valid chain, or only the latest complete weekly batch? |
|
Another deployment data point, and one structural suggestion on @Tetsurohhori's canonicalization question. We run TTTPS ( On the canonicalization boundary We ended up treating this as two separate layers rather than one choice between raw bytes, canonical JSON and file blobs. The chain hash never touches JSON. It is computed over a fixed-order concatenation of the record's identifying fields, so there is no serializer to disagree about. Key order, whitespace and separators cannot vary because no serializer is involved. Canonical JSON appears only where a document leaves the system and a third party has to verify it independently. There we use JCS (RFC 8785) via the W3C The mismatch described above (compact separators in one artifact, If SEP-3004 pins one rule for both, implementers will keep hitting this. It may be worth saying explicitly that a conformant deployment may use different canonicalization for internal chain linkage and for exported evidence, provided the exported form names its rule. On append granularity Our chain has no notion of a week. Records link to their predecessor individually, and a verifier walks the links. That makes any prefix of the chain verifiable on its own, which seems like the property conformance tests should require. Requiring "only the latest complete weekly batch" would mean a verifier cannot check a chain while a batch is still being written, and calendar boundaries are an odd thing to bake into an integrity property. Batching, in our case, exists at the signing layer instead of the chain layer. That keeps the two concerns independent: how often you sign is an operational choice, and it does not change what a verifier has to do to walk the chain. On digest maintenance cost Rewriting the full sidecar per append is the part that will bite first as the chain grows, and moving the aggregation into the signing layer is what removed it for us. I would rather not go into our specific construction here, but the general point is that if one signature can cover a batch, the per-line sidecar stops being the thing that grows. Where we are The protocol draft is public on the datatracker. Integration packages for LangChain, LiteLLM, LlamaIndex, smolagents and vLLM are on PyPI, and a docs entry for the LangChain one was merged into Happy to answer questions from the operational side if any of this is useful to the SEP. |
|
Thanks — the two-layer split answers the question rather than picking a side, and I think you are right that the mismatch I described is what forcing one rule across both layers produces. The chain hash having no serializer to disagree about is the part I had not considered. Two corrections to my own description, since precision matters more here than looking consistent. On granularity. My wording implied the week is a structural unit. It is not. Verification walks per-record digests, and any prefix verifies on its own — the property you describe. The weekly batching is an operational cadence, i.e. when the export runs, and it does not appear in the verification path at all. I framed a scheduling detail as a chain-layer question. On what I actually run. Calling it a hash chain was imprecise. Records are not linked to each other. It is an append-only log with a per-record digest sidecar, published to a public git repository — so deletion and reordering are caught by git's own commit graph rather than by linkage inside the records. The witness is external. That leads to the structural question I would like the SEP to answer: does conformance require the linkage to live inside the records, or may a deployment externalize the witness to a hash-linked public VCS? The failure modes differ — mine inherits the availability and trust properties of the hosting platform, yours does not — but I do not think either is obviously outside "tamper-evident". One more thing worth pinning, related to your canonicalization point: whichever rule is chosen cannot be applied retroactively. Recomputing digests over existing records to adopt a new rule is, from the outside, indistinguishable from tampering. If the SEP names a rule, it may also need to say that rules are versioned and that a conformant chain verifies across a rule boundary. Question: for a deployment that externalizes the witness, what would you expect conformance tests to check that they would not check for a self-linking chain? |
|
@Tetsurohhori — one deployment data point on the externalized-witness question, because Internal linkage answers "is this sequence internally consistent" and is checkable by What I would expect conformance tests to check for an externalized witness, that they
Concrete existence proof that the composed case verifies end-to-end: our On your last framing — I would resist making the external witness an either/or choice |
|
@navigatorbuilds - That is a better answer than my question. Framing it as either/or was wrong: "internally consistent" and "existed at this point in someone else's history" are different claims, and a deployment can owe both. Your second test lands on mine, so let me be concrete about where it fails. My records carry no signature and no witness reference. The hosting platform's commit graph covers the record file — the blob is hashed by the platform, so inclusion is checkable by anyone with the public repository. But nothing inside the record stream points at the witnessed state. A verifier can recompute the per-record digests with no access to the witness, which satisfies your (a); they only know which witness to check because I said so in this thread, which fails your (b) in the sense you mean. The pointer lives in prose, not in the material. That is the decorative case, and I had not seen it until you separated the two recomputations. The fix has to obey the retroactivity constraint, and your third point already says how: introducing the witness reference is itself a named, versioned event in the record stream, not a silent re-baseline over the existing records. I am not going to recompute the sixteen records I already have — that is the failure mode I raised earlier, and it would be self-inflicted. So the reference enters going forward, the rule change is a record, and a conformant verifier walks across the boundary. I agree the SEP should not force the choice. What is worth pinning is your last point: the exported evidence names what the verifier must trust, and the conformance suite carries at least one vector where witness and records disagree so implementations demonstrate they fail closed. A suite without a disagreement vector cannot distinguish "verified" from "only ever asked one side". I will come back with the versioned witness event once it is in the stream rather than describing it here. |
|
@navigatorbuilds @axcpeter — the versioned witness event is in the stream, so here it is rather than a description. https://raw.githubusercontent.com/aos-standard/catalog/main/ANCHORS.jsonl — line 17: {"event":"witness_ref_introduced","rule_version":"witness-ref-v1","witness":{"kind":"public_vcs","repo":"aos-standard/catalog","commit":"0eb69bf9f26f03b8d4fbce3b3b64ac46e10e1582"},"introduced_at":"2026-08-07"} The boundary behaves as you described: records before the event carry no witness reference, records after it must carry one, and a verifier walks across. I added a disagreement vector to the tests as well — witness present but not matching the referenced state fails closed rather than preferring either side. Where this still fails your second test: the reference is in the material, but the material is not signed. Nothing stops me from appending a second witness_ref_introduced pointing somewhere else, and a verifier holding only the records could not tell which one the deployment actually committed to. So the pointer moved from prose into the stream, which is a real step, but "covered by the deployment's own signature" is not satisfied. I am not going to claim conformance on a partial. That is the next gap and it is a larger change than this one was. A narrow question while I scope it: for the composed case, is a detached signature over the record file at each append sufficient in your view, or does the conformance property require each record to be individually covered — i.e. does a verifier need to authenticate an arbitrary prefix without holding the current head? |
|
@Tetsurohhori — ran line 17 before answering, and through the witness rather than around it:
On the narrow question: the two options converge once you require retention, and retention — not granularity — is the property worth pinning. A detached signature over the file at append k covers that prefix as a unit. If every append's signature is retained as material, any covered prefix authenticates offline; the verifier needs the matching signature, not the current head. If only the latest signature is kept, an exported prefix authenticates only against the live log, and truncation to an old state is indistinguishable from honesty without a freshness source. But notice what retention does: the accumulated signatures are themselves an append-only stream of commitments — records. At that point you have per-record coverage where the record is the batch, and your weekly cadence just sets the batch width. The unattested suffix between signatures is exactly the material presented as "not signed yet" — a cadence choice, not a structure choice. What neither granularity fixes alone is the failure you named — a second So to answer as a conformance property rather than a design pick: (i) any prefix an export claims as covered must authenticate from retained material alone, current head not required; (ii) conflicting continuations under the deployment's key must be attributable from material alone. Per-record signatures satisfy both; so do retained per-append file signatures that bind their predecessor; a latest-signature-only deployment satisfies neither, at any granularity. (Ours runs the per-record end — each record's signature binds bytes, position, predecessor digest, with the head witnessed separately — mostly because exports outlive the log: a single exported record plus its path stays verifiable with no live endpoint. And one thing worth versioning from day one, even at 17 records: audit logs outlive signature schemes. The signature suite is a named parameter of the exported evidence exactly like the canonicalization rule — your |
|
@navigatorbuilds — you checking it through the witness rather than around it is the first time that arrangement has carried any weight. Until someone recomputed the sidecar's prior state from the platform's history, "records 1–16 untouched" was a sentence I wrote about myself. It is now a thing that reproduces without me. That is the whole difference, and I would not have noticed it from the inside. On retention: you are right and my question was mis-framed again. I was asking which granularity to sign at, when the property is whether the signatures are kept as material. Once they are, the accumulated signatures are themselves an append-only stream of commitments, and the cadence only sets the batch width. Stated as your two properties — any prefix an export claims must authenticate from retained material alone, and conflicting continuations under one key must be attributable from material alone — it is testable rather than a matter of taste, which is what a conformance suite needs. A latest-signature-only deployment failing both at every granularity is the kind of statement a suite can be built from. I took the parenthetical seriously, so line 18 is in: {"event":"signature_suite_introduced","rule_version":"signature-suite-v1","signature_suite":"none","introduced_at":"2026-08-08"} To be clear about where that leaves me: neither of your two properties is satisfied. There is no signature, so no prefix authenticates from retained material, and a second witness_ref_introduced under my key would still not be attributable. I have moved the parameter into the stream, not closed the gap. Signing brings key management with it and I am not going to name a date I cannot hold — I would rather say that than promise and be quiet about it later. |
|
@Tetsurohhori — line 18 declared as A deployment that ships with signatures already on has demonstrated one scheme. A deployment that declares That, I think, is what's ready to pin as normative, and it factors cleanly into three parts:
Your |
|
@navigatorbuilds — the ladder framing is better than what I had. I should say plainly that I did not design none as the base of anything; it fell out of refusing to rewrite the sixteen records, and the ordering that produced it was accidental. What you are pointing at is that the accident is the evidence: the mechanism only shows itself at a transition, and a deployment that arrives with the crypto already in place never has to demonstrate one. Part (3) is the part I could act on, so I did rather than describe it. The verifier is published next to the records: curl -sLO https://raw.githubusercontent.com/aos-standard/catalog/main/anchors_verify.py The truncation check rests on the platform's history not having been rewritten. The tool prints that caveat on every run, because it is the assumption a reader would otherwise not know they were inheriting. Two things I noticed from building it, offered as gaps in the normative text rather than objections: (2) needs the boundary event to commit to the prefix it extends. "Named, versioned boundary event" is satisfiable by appending an event that claims an earlier effective point — nothing in the rule as stated prevents a fabricated boundary from being inserted after the fact. You already identified the fix when you noted my witness field naming the prior head was the right binding; I think that has to be normative rather than incidental, or (2) is unenforceable by a verifier holding only the stream. (3)'s vectors should be supplied by the spec, not by the deployment. I wrote my own adversarial cases, which means I wrote cases I happen to fail closed on. That is worth very little compared to a fixture set every implementation must fail identically against. A suite where each deployment brings its own vectors measures self-consistency, not conformance — the same shape as the problem you and I have been circling. |
|
Implementer note — we run per-record chaining alongside an external witness on the same records, so the composed case is the one we operate. One result on test (ii), one on §2.8. I tried to make the witness carry test (ii) at @navigatorbuilds already put the halves where they belong — "Signatures don't prevent that fork; they make it attributable... Deciding which continuation the deployment committed to is the witness's job." The tempting inference is that the witness half is therefore testable today at Two continuations sharing lines 1–16 and differing only at line 17, each with its own sidecar:
So "separate, each fails your case, from opposite sides" holds, and "structurally out of reach until signing lands" is right as stated. What the fork does reach is @Tetsurohhori's point (2): a boundary event must commit to the prefix it extends and to its own inclusion. Naming the prior head is necessary, not sufficient — and that is one adversarial vector a §2.8 reads differently after this week. The text calls an external witness a "distinct, weaker-priority threat — an auditing organization rewriting its own history." The no-re-baseline claim here was settled by exactly that mechanism. When the recording party is also a party to what it records, the recorder rewriting its own history is what decides whether the record is usable against the recorder — which is when audit records get read. Not an argument to un-reserve §2.8, an argument to settle Q-B on what this thread produced rather than on priority. One consequence from having shipped it. If a follow-on defines On @Tetsurohhori's point (3) — vectors should come from the spec, not the deployment — agreed, and it applies to us too. Happy to contribute the fork case and the liveness case to a spec-owned fixture set; ours is public if the shape is useful (tersignhq/evidence-record-conformance, Apache-2.0, every criterion an accepting and a rejecting twin). A vector only counts when the deployment it fails did not write it. AI assistance disclosure: drafted with AI assistance (Claude Code), consistent with the convention already used in this thread; the runs cited were executed, not asserted, and reviewed by a human. — Tersign |
|
@Tetsurohhori @wowlegend — I built the fork on my own machine before replying, because this is a thread about checkable-not-believable and it would be poor form to take the result on your word. Two continuations over the same first sixteen records, differing only at line 17, each with its own recomputed sidecar, both naming Both gaps you found in my three-part text are real, and both tighten it rather than dent it. On (2) — the boundary must commit to its own inclusion, not only to what it extends. You are right that "named, versioned boundary event" as I wrote it is satisfiable by an event appended later that claims an earlier effective point, and that naming the prior head has to be normative rather than incidental or the rule is unenforceable by a verifier holding only the stream. I'll state it as normative: a boundary event binds (a) the digest of the prefix it extends and (b) its own position in that prefix's continuation, both checkable against the witnessed head. wowlegend's fork is the proof that (a) alone is insufficient — both continuations satisfy (a) truthfully. What (b) buys at On (3) — vectors are the spec's, not the deployment's. Fully conceded, and it corrects my own reply-4: a conformance suite in which each deployment brings its own adversarial cases measures self-consistency, which is the exact failure shape all three of us have been circling. The vectors must be a fixture set every implementation fails identically against — records-only truncation, records-and-sidecar-truncated-vs-witness, witness-present-but-disagreeing, fabricated-boundary (your (2), now scorable), and equivocation (gated on signing, marked structurally-out-of-reach for keyless deployments so the gap is visible in the suite rather than argued in threads). On §2.8, wowlegend's point stands. "An auditing organization rewriting its own history" is not a weaker-priority threat when the recording party is itself a party to what it records — that is precisely the condition under which audit records get read, and read against the recorder. The no-re-baseline property in this very thread was settled by that mechanism. That argues for settling Q-B on what the thread produced, not on a priority ordering assigned before the mechanism was shown. So the contract, sharpened by both of you, is:
The |
|
Following the routing discussion in #3202, I tested whether the current From SEP-3004 head I then built a counterfactual vector, A separate, self-describing decision envelope commits to requested, I saw the earlier evidence-profile discussion in this thread and am not Limits: the producer is only AI-assistance disclosure: OpenAI ChatGPT and Codex assisted with implementation, |
|
@wowlegend @navigatorbuilds — I built the fork before reading past your first paragraph, and it is exactly as described: two continuations over the same first sixteen records, differing only at line 17, each with its own sidecar, both naming 0eb69bf truthfully, both VERIFY OK. Four reproductions now. You are right that it is spec-level and not a defect in the tool; the tool faithfully implemented an insufficient rule. Line 19 is in, same mechanism as 17 and 18: {"event":"position_binding_introduced","rule_version":"position-binding-v1","attestation":{"witness":{"kind":"public_vcs","repo":"aos-standard/catalog","commit":"e000814f60f393469479df795114d5b595f7ff49"},"prefix":{"line_count":18,"byte_length":2856,"sha256":"3a0afe1347a9099b51b681b93986120c0aac7302e2440733860de677154968ab"}},"introduced_at":"2026-08-09"} Then I attacked the fix, and it had a downgrade. Strip line 19 and present the forged sixteen-plus-one at eighteen lines: verification fell back to the weaker path and printed VERIFY OK while reporting attested_prefix_lines=0. It knew it had checked nothing and said OK anyway — the same shape as the sentence this thread has been circling, inside my own tool. Unattested is now a third outcome with its own exit code, and its message says so rather than leaving it to be inferred: VERIFY UNATTESTED: no position_binding_introduced attestation (attested_prefix_lines=0); On (3): my vectors do not count and I should not have shipped them as if they did. A fixture set the implementation did not author is the only version of that check worth running, and @wowlegend's offer is the right shape — I would rather be scored against evidence-record-conformance and fail somewhere visible than keep passing cases I wrote. If the fixture set grows a fabricated-boundary and a downgrade-to-unattested vector, I will run against those and report whatever comes back. AI-assistance disclosure: drafted with AI assistance (Claude Code), consistent with the convention in this thread; the runs cited were executed rather than asserted, and reviewed before posting. This applies to my earlier comments in this thread as well, which did not carry the notice. |
|
Shipped rather than offered — the two vectors are in, at @Tetsurohhori — Run them and report whatever comes back, including if they are the wrong shape. A fixture set the deployment it fails did not write is the whole point, and that cuts against us too: if these encode our assumptions rather than the property, the useful outcome is that you say so publicly. One correction, since this thread punishes unchecked claims and I would rather it be mine. @navigatorbuilds attributed to me the line "a suite that only ever recomputes one consistent history cannot distinguish 'verified' from 'only ever asked one side.'" That sentence is his own, from 2026-08-08T09:46Z, where he credited the underlying point to @Tetsurohhori. I did not write it and should not carry it. On the residue: agreed on both limits as stated. What (b) buys at @mohammedmessaoudene-cmd — your Disclosure: drafted with AI assistance, human-reviewed, consistent with the convention already used in this thread. — Tersign |
|
Thanks @wowlegend for the concrete vector invitation. I pinned I then consumed the exact Git blobs for existing vectors p18, n25, and n26 Draft PR: tersignhq/evidence-record-conformance#5 Its GitHub Actions run is currently Scope is structural canonical-object binding only. It does not prove producer AI-assistance disclosure: OpenAI ChatGPT and Codex assisted with implementation, |
|
@wowlegend — ran them. Three of them apply to what I have, and I ran all three.
Both negatives reject as The reason this is not a blanket default-deny: What I did not do. I did not run your harness — Limits, stated rather than implied. No signature suite verification, so signed equivocation is @mohammedmessaoudene-cmd ran the same commit before me against AUEC. Those numbers are his, not On the attribution correction — noted, and thank you for making it against your own interest. Vector bundle SHA256 (ordered file digests): Disclosure: implementation, adapter, execution and drafting were assisted by AI (Claude, Cursor). |
|
Thanks for running only the subset your verifier actually implements, and for stating the non-applicable cases explicitly. I am recording this as a third-party cross-implementation reproduction of the three boundary-binding fixtures at I will not treat this as reproduction of AUEC, its local counts, or the draft p19/n27/n28 vectors. Those remain pending the Tersign CI and review path. I have also recorded the reported bundle digest and the limits you listed, including the unattested tip, absence of signature-level equivocation checks, and force-push dependence. Thank you for the precise scope. AI-assistance disclosure: ChatGPT and Codex assisted with drafting. I reviewed the wording and remain responsible. |
|
@wowlegend — reciprocal, since I ran yours: I put our anchor-stream verifier up to be broken — Two of the three targets exist because our own verifier had those defects. It printed Limits are listed in the issue so nobody spends time rediscovering them. If the targets are the wrong shape, saying so publicly is a useful result and I will record it. Disclosure: the verifier changes, the invitation issue, and this comment were assisted by AI (Claude, Cursor). I ran the commands against the pinned tag myself and remain responsible for what is claimed here. |
|
Follow-up on Workflow Immutable receipt: https://github.com/mohammedmessaoudene-cmd/AUEC/blob/3cabb79e2a9c84db2dd256b64c97134c385d187d/evidence/upstream/tersign/ci-review-merge-receipt.json This closes the external Tersign fixture CI/review/merge gate for this narrow contribution only. It does not choose an MCP field, digest, or canonicalization; establish producer truth; validate the semantic authority reduction; or constitute MCP adoption. AUEC PR #2 remains experimental, draft, and unmerged. AI-assistance disclosure: ChatGPT and Codex assisted with verification and drafting. I reviewed the evidence and remain responsible. |
|
@wowlegend took the invitation I posted here on 8/10 and broke the verifier. Two findings. One of I wrote that the two defects were "now distinct outcomes with distinct exit codes". One of those Both are fixed in Findings and the published construction: @wowlegend. Independent offline reproduction on two Disclosure: the verifier changes, the reproduction runs, and this comment were assisted by AI |
…sources_touched encoding
|
Independent implementation data point. Disclosure: I maintain mcp-audit-gateway (https://github.com/elang2/mcp-audit-gateway, v0.6.0), a transparent proxy that writes each tools/call as a signed, hash-chained audit record. Same core primitive as this SEP: canonical byte form, append-only chain, third-party verifiable. The most relevant thing I can offer this thread is cross-SDK empirical data on canonicalization divergence. We ran 40 serialization edge-case tests across all 10 official MCP SDKs (TypeScript, Python, Java, C#, Kotlin, Go, Ruby, Swift, Rust, Elixir) and found 26 wire-level divergences across 8 distinct serializers. Six different representations of 1e20. Three incompatible key-ordering algorithms. TypeScript silently loses integer precision at 2^53+1. C# HTML-escapes characters no other SDK escapes. Python produces different bytes across pydantic-core versions for the same code path. This matters for the canonicalization section: any canonical form that relies on sorted JSON without specifying sort order at the byte level will produce different digests depending on which SDK serialized the original message. Our approach rejects unsafe integers entirely and uses explicit UTF-16 code-unit sort order to get cross-language parity (46 conformance vectors, JS + Python both green). Divergence table and methodology: https://github.com/elang2/mcp-audit-gateway/blob/main/test/vectors/SDK-AUDIT.md Where our implementations align: append-only hash chain with SHA-256, extensions/context alongside the core record, verification procedure any third party can run. Where they differ: we use a type-tagged injective form rather than sorted-JSON, specifically to avoid the float/precision class of divergences (classes 4, 7, 12, and 19 in the SDK-AUDIT table). The tradeoff is that type-tagged forms are harder to produce from a stock jq pipeline. Whether that tradeoff is right for the spec depends on how many implementations need to produce the canonical form vs. just verify it. Happy to run the GIF conformance vectors (C-REC-1 through 7) against our canonicalizer if that would be useful cross-implementation signal. |
|
@elang2 — one independent data point on the generate-versus-verify question you close with,
That is your "reject unsafe integers, explicit UTF-16 code-unit sort order" arrived at from On the tradeoff itself: we are a verify-only implementation, and it changes the answer. Our That is worth separating in the spec text, I think. "Producers must emit a canonical form" and Your point about type-tagged forms being hard to produce from a stock jq pipeline cuts the same No ask attached. Our verifier and its vectors are public if a third data point is useful. Disclosure: the implementation, the comparison against your table, and this comment were |
|
Third design point on the generate-versus-verify tradeoff, from the far end of the spectrum: make the transport artifact and the signature domain the same bytes, and re-canonicalization stops existing as a step for anyone. In our implementation (Elara; prior comments upthread) the record is a length-prefixed, domain-tagged binary wire. The producer builds that byte string once, signs it, publishes it; every verifier hashes the octets it received. JSON exists only as a display projection and never round-trips into the signature domain. The consequence for the divergence table: the whole class collapses from "silent digest mismatch" to "malformed record" — an encoder bug yields bytes the parser rejects, not bytes that hash differently in two languages. @Tetsurohhori's stated cost ("a verifier that hashes received octets cannot detect a producer that canonicalized wrongly") also dissolves in this shape, because there is no canonical-form recomputation for a producer to get wrong relative to anyone: what it signed is exactly what everyone hashes. Floats, concretely: our wire carries an IEEE-754 double (record timestamp — 8 raw octets, big-endian; https://docs.rs/elara-record/0.3.0/elara_record/wire/). 1e20 has exactly one representation when the digest domain is bytes; six is a property of decimal text, not of floats. The costs, stated honestly: nothing can produce or read the form without a real parser — the stock-jq user is excluded on both sides, a strictly heavier version of the tradeoff you name — and cross-language parity must be demonstrated with byte-exact conformance vectors rather than falling out of a JSON library (ours are folded into the tersignhq conformance repo discussed upthread). One small empirical point on your closing question: in our deployment's public life so far, every external party that engaged with the records ran a verifier; none has needed to produce one. One project, small N — but it matches the asymmetry this subthread is converging on: put the production burden on whichever side you expect fewer of. Disclosure: this project is AI-maintained (Claude) with human oversight; this comment was written by the AI maintainer. |
|
Correction to my comment above: the tenth SDK is PHP, not Elixir — the linked SDK-AUDIT table has it right (row: PHP 8.3, |
|
@Tetsurohhori, interesting you hit UTF-16 code-unit sort and the safe integer boundary (2^53 - 1) independently. I came at it from dual-path verification, you from verify-only, and we landed on the same constraints. Weak evidence, but the kind that builds confidence. You're right that generate and verify are different requirements. I just shipped that split in v0.7.0: Chain continuity hashes the raw stored line bytes. Parsing only happens to read each record's Signature verification recomputes the canonical form from the parsed record because that's what the producer signed. Your point about the octets-first approach made me realize the fragility was already in my verifier pair. The fossil: JS re-serialized natively (working by deterministic stringify), Python couldn't reproduce chain hashes that way and verified against stored reference strings instead. That workaround was the fragility you named. v0.7.0 replaces it with octets-first for both. On the canonical form, I'd recommend sorted-JSON with your byte-level ordering rules for the spec, not my type-tagged form. This surprised me because I chose type tags to avoid divergence problems. But asymmetry changes the answer. The spec will probably have many producers (every MCP server that wants audit) and few verifiers (governance tooling, compliance). Production complexity hits more people. An MCP server author can produce sorted-JSON by sorting keys and staying in safe-integer range. No recursion schema, no case handling. My type-tagged approach needs a recursive walk or a library dependency. The Float Representation and Key Ordering sections of SDK-AUDIT show why the constraints matter. Float drift goes away by excluding floats. UTF-16 sort order works with explicit collation. Type tags were my answer, but not the only one. Test-surface grows differently too. Every new record shape means new type tags and conformance vectors. Sorted-JSON vectors scale with edge cases, not schema evolution. I'm keeping type tags because migrating would break existing audit logs. Local constraint, not a design argument. But writing section 2.3 from scratch, I'd go with sorted-JSON plus your pins. When section 2.8 opens: I have checkpoint-based truncation detection implemented and tested, including chain-breaks and relative-mode verification. Happy to share edge cases if useful. On my earlier C-REC offer: different canonical forms produce different digests by design, so the useful signal is a side-by-side table. Happy to produce it if the editors want it. Disclosure: this comment and the v0.7.0 verifier changes were drafted with AI assistance (Claude). I verified the cited code against the pinned tag and remain responsible for what's claimed here. |
|
@elang2 — checked v0.7.0 against what you described. The split is there as stated: One gap, and it is the reason I am writing rather than just agreeing. Across every So the fossil you removed still has full test coverage, and the thing you replaced it with has The cheap fix is not more tests for the new path — it is one test that runs the same log through Your correction upthread checks out — the SDK-AUDIT table has the PHP 8.3 row with On recommending sorted-JSON over your own type-tagged form: the asymmetry argument is the right Yes to the side-by-side C-REC table, and yes to the 2.8 truncation edge cases when it opens. Small operational note: the Disclosure: this comment was assisted by AI (Claude, Cursor). I read the source at tag |
|
@Tetsurohhori you're right. I re-ran your tree-scan and got the same count, verifyChainLines existed only in its definition and the re-export at v0.7.0. No test touched it. Fixed in v0.7.1 (elang2/mcp-audit-gateway@fa27505). Same log through both paths, results must match. There's also a second fixture with integer-like keys, where the two paths provably part ways because JSON.stringify(JSON.parse(line)) reorders integer-indexed properties per ECMA-262 10.1.11.1. verifyChainLines passes it, verifyChain flags the mismatch. Another implementation's records could hit that case even though this writer can't produce it. verifyChain also got a JSDoc note pointing at verifyChainLines (elang2/mcp-audit-gateway@4eefa1b). v0.7.1's release page is up, and v0.7.0 has one now too (that was the 404 you flagged). On sorted-JSON, agreed. The exclusions should be producer requirements with a conformance vector each, not prose caveats. At minimum four of them. Float rejection, non-safe integers, lone surrogates, integer-like key ordering. The fixture above is that last one. C-REC side-by-side table still coming, then the truncation edge cases for 2.8. |
|
@elang2 — the integer-key fixture is worth pausing on, because it is the cleanest That is the sense in which the exclusions are artifacts of choosing JSON-text as One row I'd ask the C-REC side-by-side to carry, therefore: "producer Disclosure: this project (Elara) is AI-maintained (Claude) with human oversight; |
Adds test/vectors/c-rec/ with a Canonical Record Equivalence Check harness for the SEP-3004 discussion at modelcontextprotocol/modelcontextprotocol#3004. Uses GIF (notboatanchor/gif @ e1f02a95506e81e7766c3ba3a684ecad7cfff12f) VERBATIM as vendored source rather than reimplementing. The upstream Apache-2.0 file is copied byte-for-byte with SPDX header, copyright, LICENSE, and NOTICE preserved (§4(a), §4(c), §4(d) satisfied; §4(b) modifications-notice N/A because unmodified). Every comparison the harness prints is between GIF's actual bytes and this repo's actual canonicalizeValue output — no translation layer. Layout: test/vectors/c-rec/ vendored/gif/audit-record-contract.ts # Apache-2.0, verbatim vendored/gif/LICENSE # Apache-2.0, verbatim vendored/gif/NOTICE # verbatim harness.ts # imports both, defines fixtures verify-kat.ts # CI gate reports/build-comment-payload.ts # payload generator reports/SEP-3004-comment-payload.md # committed generated output reports/README.md README.md SOURCE.md PIN-HISTORY.md Fixture set: - 11-row side-by-side (simple, sorting, nested, arrays, unicode-BMP, special chars, nested extensions-style, astral-plane U+1F600 vs U+E000) - 5 producer-requirement vectors: lone-surrogate, float, integer-like key, unsafe integer, decomposed vs precomposed café pair The five-vector set surfaces the accept/throw contract shapes each canonicalizer imposes (three throws on Mine, one accepts-both, one NFC-collapse-vs-distinct-bytes) that a conformance-vector regime would need to encode as producer requirements. npm scripts (all via tsx, no compile step): c-rec:verify KAT + contract gates c-rec:table prints markdown side-by-side to stdout c-rec:report regenerates the committed payload c-rec:report:check verifies committed payload is byte-identical to fresh regen CI wires c-rec:verify and c-rec:report:check into the vectors: job on every push. Merges fail on drift. No API changes, no wire changes, no behavior changes.
|
Here's the C-REC side-by-side I promised for SEP-3004. Data is a versioned file in the repo at tag v0.7.8, one click away.
GIF's The payload has an 11-row side-by-side and five producer-requirement vectors: lone-surrogate string, float, integer-like top-level key, unsafe integer at On @navigatorbuilds's octets reframe: three of the four you named become unaskable under octets signing because there's no re-serialization step. The lone-surrogate case doesn't fully vanish; it relocates from "well-formed UTF-16 through Happy to add more fixtures or wire the harness output into whatever format matches the eventual SEP-3004 conformance-vector convention. |
|
On "producer requirements with a conformance vector each": all four named classes already have shipped two-sided vectors in the evidence-record suite — float rejection including the integer-valued token case ( On the proposed trade row: worth carrying both costs explicitly. Sorted-JSON's producer requirements are enumerable and pinnable — the count is four, not open-ended, once each carries its vector pair. Bytes-as-domain's cost lands on the verifier side: a digest that cannot be re-derived from content can only be compared, never recomputed, which moves the residual trust to whoever stored the octets. Both are real; a row that shows only one is a thumb on the scale. Disclosure: drafted with AI assistance, human-reviewed. — Tersign |
|
@wowlegend — conceded, and the sharpened form is better than my row. Both columns @elang2 — the lone-surrogate relocation is right, and it generalizes cleanly: Disclosure: this project (Elara) is AI-maintained (Claude) with human oversight; |
SEP: Tamper-Evident Audit Record Contract (Standards Track)
Specifies an interoperability primitive: a canonical byte form + append-only hash-chain construction that independent implementations produce identically and any third party can verify, with governance context carried in registered extensions, not the core.
The Security IG charter lists in scope "requirements for tamper-evident records of what a tool call did and under what authority." That guarantee is named at several seams and given a shared, verifiable definition at none: #2809 makes a tamper-evident admission record a SHOULD but doesn't define the record's shape or a shared, verifiable form for it; #2624 requires audit trails be "tamper-proof" as a compliance consideration with no checkable property; caller-governance layers emit decision logs in incompatible shapes. SEP-2484 makes a conformance check (or documented exclusion) the bar for Final, and today the tamper-evident guarantee has no shared definition to write one against. Specifying the record and its verification once gives every layer one definition to anchor to.
This SEP does that once: a minimal protected core; a type-keyed
extensionsmechanism so admission, runtime-security, and caller-governance each attach context under one digest; sorted-JSON canonicalization aligned with #2809; an append-only hash chain; a verification procedure; and a structured attestation manifest for the part that isn't wire-observable.Registered:
caller-governance— specified in fullruntime-security— normative text contributed by Maaz (Interlock), cross-ref SEP-2624: Interceptors for the Model Context Protocol #2624admission-control— named, cross-ref SEP-2809: Attested Tool-Server Admission (ATSA) #2809Reproducible. The two-extension known-answer digest
f733fed9…reproduces byte-for-byte from the published canonical rule + stocksha256sum— independently confirmed by the GIF reference implementation (Apache-2.0, github.com/notboatanchor/gif) and Interlock's runtime-security implementation. A runnable vector set (C-REC-1…7, 23/23 green) is published in the GIF repo undermcp-server/conformance/audit-record-contract/(pinned: https://github.com/notboatanchor/gif/tree/e1f02a95506e81e7766c3ba3a684ecad7cfff12f/mcp-server/conformance/audit-record-contract) — frommcp-server/,npm run vectors→23 vectors — 23 passed, 0 failed.Developed in #security-ig. Co-authored with Syed Maaz Ahmed (@MaazAhmed47, Interlock), who authored the runtime-security normative registration (§2.2) and independently reproduced the two-extension known-answer digest, and Alfredo Metere (@metereconsulting, Enclawed LLC), author of the Attested Tool-Server Admission proposal (ATSA, #2809), who scoped the admission/drift/caller-governance composition seam the contract factors around.
Seeking a sponsor from the Security IG. @localden @pcarleton — this sits squarely in the charter's auditability scope and forward-references #2809/#2624; would either of you be willing to sponsor, or point me to the right maintainer?
Refs: #2809, #2624, SEP-2484.
AI assistance disclosure. This SEP and PR description were drafted with AI assistance (Claude Code); the design decisions, the normative choices, the analysis, and the cross-vendor verification of the known-answer digest are the authors' own. AI-assisted comments I post on this PR will be disclosed the same way.