Skip to content

SEP-3004: Tamper-Evident Audit Record Contract - #3004

Open
scottrhodes wants to merge 4 commits into
modelcontextprotocol:mainfrom
scottrhodes:sep-tamper-evident-audit-record-contract
Open

SEP-3004: Tamper-Evident Audit Record Contract#3004
scottrhodes wants to merge 4 commits into
modelcontextprotocol:mainfrom
scottrhodes:sep-tamper-evident-audit-record-contract

Conversation

@scottrhodes

Copy link
Copy Markdown

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 extensions mechanism 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:

Reproducible. The two-extension known-answer digest f733fed9… reproduces byte-for-byte from the published canonical rule + stock sha256sum — 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 under mcp-server/conformance/audit-record-contract/ (pinned: https://github.com/notboatanchor/gif/tree/e1f02a95506e81e7766c3ba3a684ecad7cfff12f/mcp-server/conformance/audit-record-contract) — from mcp-server/, npm run vectors23 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.

@scottrhodes
scottrhodes requested review from a team as code owners July 2, 2026 21:57
@kuangmi-bit

This comment was marked as spam.

@scottrhodes

Copy link
Copy Markdown
Author

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.
A second-language reproduction of the vectors needs no coordination at all - independent reproduction is the point of the construction. The two-extension KAT in §Conformance is self-contained (canonical preimage + sha256sum), and the full C-REC set is published at the pinned link in the SEP. A Go implementation landing on the same digests would be exactly the kind of evidence this contract exists to make possible.

§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.

@kuangmi-bit

This comment was marked as spam.

@kuangmi-bit

This comment was marked as spam.

@kuangmi-bit

This comment was marked as spam.

@matssun

matssun commented Jul 15, 2026

Copy link
Copy Markdown

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.

@scottrhodes

Copy link
Copy Markdown
Author

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.

@navigatorbuilds

Copy link
Copy Markdown

Independent reproduction, since the thread says that half needs no coordination: I
reimplemented the gif-audit/2 canonical form in Python from the specification text
rather than porting the reference, and it lands on both published known-answer digests
(d494769c…, f733fed9…) from their published preimages. The construction holds.

Two things fell out that may be worth something to the SEP itself.

A three-extension known-answer vector.

81aea291a50bdfd0e0db10e64479117cac9a19126cb28311dbaef70a8efa46b3

caller-governance + runtime-security + a third extension under one digest, where the
first two bodies are byte-identical to the §Conformance two-extension fixture — so the
only delta from f733fed9… is the third extension. Reproducible from a printed preimage
with printf … | sha256sum. Yours to adopt as a C-REC vector if a 3-extension case is
worth pinning. Adding the third type cost exactly one entry in PROFILES: the
unpatched reference verifier rejects the record before that entry and validates, hashes
and chains it after. That is the "new emitter types add an extension, not a new chain"
claim exercised rather than restated.

One thing the core structurally cannot carry. §2.1 makes occurred_at the recorder's
clock and explicitly not caller-settable — right for a tamper-evident record, but it means
the core can attest when the host wrote an event down, never when the caller acted. A
worked pair here: the same principal, the same mandate, the same signed act payload, 166 ms
apart, straddling the principal's revocation. Every protected core field is identical
except event_id, occurred_at and the chain link. Both records verify. Both are
honestly outcome: allowed, because the host did dispatch both calls. SEP-3004
verification cannot separate them — and shouldn't have to; that is the layering working as
designed. It does mean caller-side signed time has to live in an evidence extension, and
that such an extension is only useful if it is allowed to contradict the record it rides
in. One that could only ever agree with outcome would carry no information.

@matssun — the decomposition you sketched (evidence profile identifier, digest, optional
reference) is what I independently ended up with, which I take as a fair sign it is the
right cut. It also has a concrete home now that it lacked when you wrote it: the
2026-07-28 spec ships the formal extensions framework — vendor-prefixed extension
identifiers, an Extensions Track in the SEP process. I am not proposing a
registration.
That seam is MCP-RE's and it should land there. What I have is an instance of it running against real artifacts — post-quantum
signed delegation with a revocation lifecycle, verdicts from a signing-incapable offline
verifier — if a second data point is useful while you are specifying it. If your
registration subsumes the authority axis under a different type id or field set, this
remaps to it; the part I would argue for keeping either way is the conformance direction:
recomputability, not agreement — an independent party MUST be able to re-derive the
verdict from the digest and the artifact, rather than trust the emitter's assertion of it.

Caveats, plainly: the acts come from a small self-hosted testnet, not a production network,
and the payload they carry is illustrative — the signatures, mandate, revocation and
verdicts are real. Everything above recomputes from committed bytes in one repo, two
commands, no chain interaction and no trust in me:
https://github.com/navigatorbuilds/x402-elara-demo/tree/main/conformance/mcp-3004-audit-record-v0

AI assistance disclosure: built and verified with AI assistance (Claude Code), consistent
with the disclosure convention already used in this thread. The runs cited were executed,
not asserted.

@scottrhodes

Copy link
Copy Markdown
Author

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.

@localden localden changed the title SEP: Tamper-Evident Audit Record Contract SEP-3004: Tamper-Evident Audit Record Contract Jul 29, 2026
@localden localden added SEP proposal SEP proposal without a sponsor. labels Jul 29, 2026
@Tetsurohhori

Copy link
Copy Markdown

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 (separators=(",", ":"), no spaces). That differs from other artifacts in the same deployment that canonicalize with sort_keys=True. Interoperability question: should SEP-3004 pin whether digests cover raw record bytes, canonical JSON objects, or file-level blobs?

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?

@axcpeter

axcpeter commented Aug 5, 2026

Copy link
Copy Markdown

Another deployment data point, and one structural suggestion on @Tetsurohhori's canonicalization question.

We run TTTPS (draft-helmprotocol-tttps, currently rev 08 on the IETF datatracker), a timestamp-receipt protocol for AI agent output. It has been serving a public self-serve API for a few weeks; 309 receipts sealed as of writing. Not a large deployment either, but it has been through a few of the questions raised here.

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 eddsa-jcs-2022 cryptosuite, and we check our implementation against the vc-di-eddsa test vectors so that an outside verifier with no knowledge of our code can reproduce the digest.

The mismatch described above (compact separators in one artifact, sort_keys=True in another, same deployment) is what happens when both layers share one canonicalization rule. Splitting them removes the question rather than answering it: the integrity chain has nothing to canonicalize, and the exported document follows an existing specification that verifiers already implement.

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 langchain-ai/docs (#5257) last week. Issuer keys are published as a did:web document so certificates verify offline.

Happy to answer questions from the operational side if any of this is useful to the SEP.

@Tetsurohhori

Copy link
Copy Markdown

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?

@navigatorbuilds

Copy link
Copy Markdown

@Tetsurohhori — one deployment data point on the externalized-witness question, because
we run the composed case: linkage inside the records and an external witness, and the
two turned out to be answers to different questions rather than rivals.

Internal linkage answers "is this sequence internally consistent" and is checkable by
anyone holding only the records. An external witness answers "did this state exist at
this point in someone else's history" and is checkable by anyone holding only the
witness. Externalizing the witness doesn't weaken tamper-evidence; it changes whose
availability and honesty you inherit — which is why it belongs in the exported
evidence's trust statement, the same way axcpeter's exported form names its
canonicalization rule.

What I would expect conformance tests to check for an externalized witness, that they
would not check for a self-linking chain:

  1. Two independent recomputations, neither sufficient alone. A verifier must be able
    to (a) recompute the record digests with no access to the witness, and (b) verify
    witness inclusion with no access to the deployment that produced the records. If
    either check silently requires the other party, the witness is decorative.
  2. The witness reference must be inside the signed material. If the pointer to the
    witnessed state is not covered by the deployment's own signature, a deployment can
    re-point it after the fact and the two checks pass individually while attesting
    different histories.
  3. Rule and witness versioning across boundaries — your retroactivity point
    generalizes: a conformant chain should verify across a canonicalization-rule change
    and across a witness change (platform migration), which forces both to be named,
    versioned events inside the record stream rather than silent re-baselines.

Concrete existence proof that the composed case verifies end-to-end: our
witnessed-anchor-v0 verifies two independent anchors — ours and one produced by an
unrelated project that anchored our revocation envelope into their own chain — under
one signed head, from a bare clone, exit 0:
https://github.com/navigatorbuilds/elara-mesh (witnessed-anchor-v0; head 6492 covers
both anchor directions). The failure mode it exists to catch is exactly your
"recomputing digests is indistinguishable from tampering": a re-baseline cannot cover
the foreign anchor, so the composed verification fails closed.

On your last framing — I would resist making the external witness an either/or choice
in the SEP. The property worth pinning is that the exported evidence names what the
verifier must trust (a platform's commit graph, a signer set, both), and that the
conformance suite contains at least one vector where the witness and the records
disagree, so implementations prove they fail closed rather than preferring one side.

@Tetsurohhori

Copy link
Copy Markdown

@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.

@Tetsurohhori

Copy link
Copy Markdown

@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"}
Records 1–16 are byte-identical to what they were before; only line 17 and its sidecar digest were added. That claim is itself checkable through the witness — the platform's history for the digest file shows the previous list, so "I did not re-baseline" is verifiable by someone who does not trust me, which is the first time the witness has done any work here beyond being asserted.

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?

@navigatorbuilds

Copy link
Copy Markdown

@Tetsurohhori — ran line 17 before answering, and through the witness rather than around it:

  • 17 lines at main; per-line SHA-256 recomputed against the sidecar: 17/17 match.
  • The sidecar's history has exactly one commit between your earlier description and now. At 0eb69bf — the commit your witness field names — it held 16 entries, byte-identical to the current first 16. So "records 1–16 untouched, only line 17 and its digest appended" reproduces from public material without trusting anything said in this thread. Your witness reference naming the prior head is the right binding, incidentally: the event commits to the exact state it extends.

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 witness_ref_introduced under the same key. Signatures don't prevent that fork; they make it attributable: two valid signatures by one key over conflicting continuations of the same prefix is mechanical proof of equivocation, checkable by anyone holding both. Deciding which continuation the deployment committed to is the witness's job — which is why the reference must sit inside the signed material, and the witness's linear history arbitrates. Composed, each claim closes the other's gap; separate, each fails your case, from opposite sides.

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 rule_version boundary is how a post-quantum migration enters without the re-baseline trap.)

@Tetsurohhori

Copy link
Copy Markdown

@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"}
Declared as none, which is the honest current value. The point is only that the suite is now a named parameter of the stream rather than an unstated assumption, so whenever signing does land — including a post-quantum change later — it enters as a boundary event next to the canonicalization rule instead of a silent re-baseline. Records 1–17 are unchanged; the same check you ran should still hold.

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.

@navigatorbuilds

Copy link
Copy Markdown

@Tetsurohhori — line 18 declared as none is the right move, and it's worth being precise about why the null is not a placeholder but the base of the ladder.

A deployment that ships with signatures already on has demonstrated one scheme. A deployment that declares signature_suite: none and later introduces, say, signature-suite-v2 through a boundary event has demonstrated the evolution — that a scheme enters the stream without rewriting the records that predate it. The second is the harder property and the one the contract actually needs, because audit logs outlive signature schemes and the migration is exactly where the re-baseline trap springs. So none → something, proven across the boundary by a verifier who walks it, is stronger evidence for the SEP than something alone. You now have line 17 and line 18 as two instances of the same mechanism at different parameters — witness reference and signature suite — which is enough to show the mechanism is general, not special-cased to one field.

That, I think, is what's ready to pin as normative, and it factors cleanly into three parts:

  1. The contract — your two tests: (i) any prefix an export claims as covered authenticates from retained material alone, current head not required; (ii) conflicting continuations under the deployment's key are attributable from material alone. This is the tamper-evidence guarantee, stated independently of which crypto satisfies it.

  2. The evolution rule — parameters of the exported evidence (canonicalization, witness binding, signature suite) change only through named, versioned boundary events in the stream; records before a boundary are never rewritten to match it. This is what makes (1) survivable across a scheme's whole lifetime, post-quantum migration included.

  3. The conformance suite — must carry adversarial vectors that fail closed, not only happy-path recomputation: witness-present-but-disagreeing (you added this), equivocation (two valid continuations under one key over the same prefix), and truncation-to-an-old-state with no freshness source. A suite that only ever recomputes one consistent history cannot distinguish "verified" from "only ever asked one side" — which was your point from the start, and it's the sentence the suite section should quote.

Your none and per-record signatures are then the two honest endpoints of property (1) — you at the base of the ladder with the evolution mechanism proven and the crypto pending, a per-record deployment at the far end with the crypto in place — and the SEP's job is to make both, and everything between, checkable against the same three parts rather than against each other.

@Tetsurohhori

Copy link
Copy Markdown

@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
python3 anchors_verify.py --self-test
python3 anchors_verify.py
--anchors-url https://raw.githubusercontent.com/aos-standard/catalog/main/ANCHORS.jsonl
--digests-url https://raw.githubusercontent.com/aos-standard/catalog/main/ANCHORS.jsonl.digests.json
Standard library only, no signature verification since there is none to verify. --self-test constructs the tampered inputs and shows them failing closed on your machine rather than on my word; file:// URLs work if you want to build your own. Of your three vectors: witness-disagreement and truncation are in — both records-only truncation, and records-and-sidecar truncated together, which passes internal consistency and is caught only against the witness. Equivocation is not in, because there is no key to equivocate with. So one of three is structurally out of reach for me until signing lands, and I would rather that gap be visible in the tool than argued about here.

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.

@wowlegend

Copy link
Copy Markdown

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 signature_suite: none. It does not.

@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 none. @Tetsurohhori's tool invites building your own inputs, so I checked instead of arguing.

Two continuations sharing lines 1–16 and differing only at line 17, each with its own sidecar: anchors_verify.py accepts both, and neither had to be committed to the witness. Two reasons, both spec-level rather than defects in the tool:

  • The witness reference names the prior head, so it certifies what a continuation extends — not that the continuation itself was witnessed. Two conflicting line 17s both name 0eb69bf truthfully.
  • The only check reaching the platform is a line-count lower bound, so any fork at or above the witnessed length passes; and the repo consulted is the one named by the record under verification.

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 none deployment can be scored against with no key in hand.

§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 anchor_witness as an optional field and stops there, a deployment can present a fully green chain with a dead witness lane for weeks — nothing in the record distinguishes no witness configured from witness configured and failing, because the failure is an absence and no chain check sees one. Ours went quiet once; we measure it separately now. Worth making a declared witness's liveness observable in the exported evidence rather than only at configuration time.

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

@navigatorbuilds

navigatorbuilds commented Aug 8, 2026

Copy link
Copy Markdown

@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 0eb69bf as prior head: anchors_verify.py returns VERIFY OK for both. So the conclusion holds three independent ways now — your tool, wowlegend's build, and mine — and that is worth more than any of us restating it.

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 signature_suite: none, with no key in hand, is detection of a fabricated boundary inserted after the fact: that is the one adversarial vector a keyless deployment can be scored against, and it should be in the suite as such. It does not buy detection of two equally-valid continuations presented to different auditors — that remains out of reach until signing makes the fork attributable. Naming both limits honestly is the point, not a hedge.

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:

  1. Tamper-evidence — (i) any prefix an export claims as covered authenticates from retained material alone; (ii) conflicting continuations under the deployment's key are attributable from material alone (keyless: out of reach, and the suite must say so).
  2. Evolution — exported-evidence parameters change only through boundary events that bind both the prefix digest and their own position; pre-boundary records are never rewritten. Necessary and now sufficient against fabricated-after-the-fact boundaries; sufficient against equivocation only with signing.
  3. Conformance — a spec-supplied fixture set of adversarial vectors that every implementation fails closed on identically; deployment-authored vectors measure self-consistency and do not count.

The none deployment and a per-record-signed one are then the two honest endpoints of (1), and the suite scores both — and the fabricated-boundary vector — against the same three parts. The one line I'd keep visible in the suite itself is wowlegend's: a suite that only ever recomputes one consistent history cannot distinguish "verified" from "only ever asked one side."

@mohammedmessaoudene-cmd

Copy link
Copy Markdown

Following the routing discussion in #3202, I tested whether the current
caller-governance registration preserves a requested-to-effective authority
reduction.

From SEP-3004 head 377f8d260d, a clean-room Python verifier reproduces the
published two-extension KAT
f733fed9cc757165f810b778e4baba1f51a45504988e937707aaab4361b2f064
and passes all 23 C-REC vector cases. The published Node runner also passes 23/23
against both the pinned and current vector sets.

I then built a counterfactual vector, CG-DELTA-LOSS-01, holding every
record-visible input fixed. The same action requests a node budget of 10. Policy
A permits 10; policy B permits 5. Both outcomes are allowed, but their policy
digests and requested-to-effective deltas differ. With the currently registered
caller-governance fields, the protected records are byte-identical.

A separate, self-describing decision envelope commits to requested,
host-allowed, effective and delta values; policy id/version/digest; principal;
action and input digests; distinct decision-authority and record-emitter ids;
and verdict/reason codes. As a diagnostic only, adding that envelope's digest
as one optional decision_evidence_hash under caller-governance makes the
pair distinct. The R48 clean-room verifier rejects that unregistered field; the
published TypeScript runner is permissive about unknown internal fields, so I
do not treat rejection as a cross-implementation property.

I saw the earlier evidence-profile discussion in this thread and am not
proposing a duplicate. For this narrower authority-delta case, would an optional
decision_evidence_hash in caller-governance be useful, or should the envelope
remain implementation-local? I can prepare a focused patch and vectors if
useful.

Limits: the producer is only self_attested; this does not establish producer
truth or external observation. The draft implementation and evidence are in
AUEC PR #2.

AI-assistance disclosure: OpenAI ChatGPT and Codex assisted with implementation,
testing, analysis and drafting. I reviewed the executed evidence and remain
responsible for this comment.

@Tetsurohhori

Copy link
Copy Markdown

@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"}
The ordering problem resolves by making the binding retrospective: a record cannot name the commit that will contain it, so each append attests the commit containing the previous one. The verifier now fetches the file at the attested commit and compares the presented prefix byte-for-byte instead of checking a length lower bound, and the witness repository is supplied by the verifying party via --expect-witness-repo rather than read out of the stream under verification — your second reason was the sharper of the two, since a forged stream naming an attacker's repository was choosing its own trust anchor.

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);
digest and boundary checks alone are insufficient — offline snapshots cannot be
distinguished from verified streams
What this does not close, stated as limits rather than caveats: a fork at the tip, since the newest append is unattested until the next one arrives; equivocation, which needs a key; and force-push, which takes the witness with it. Your framing holds — (b) buys detection of a boundary fabricated after the fact, not discrimination between two equally valid continuations shown to different auditors.

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.

@wowlegend

Copy link
Copy Markdown

Shipped rather than offered — the two vectors are in, at 46ad663.

@Tetsurohhorin25-boundary-prefix-only-no-position is the fabricated-boundary case and n26-coverage-claimed-over-empty-attestation is the downgrade you found in your own tool. p18 is the accepting twin. They are stated at the record layer — a prefix, a boundary event, a claimed covered position — so nothing depends on ANCHORS.jsonl, on a witness kind, or on a signature suite; an implementation can be scored without adopting anyone's shape. Both engines agree on all three, and the differential harness runs them plus an off-corpus mutation battery.

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 signature_suite: none is detection of a boundary fabricated after the fact, and not detection of two equally-valid continuations shown to different auditors — that needs the key, and naming the limit is worth more in a suite than a vector that pretends otherwise. If the equivocation fixture lands, marking it structurally-out-of-reach-without-signing rather than omitting it is the right call: the gap becomes visible in the artifact instead of arguable in a thread.

@mohammedmessaoudene-cmd — your CG-DELTA-LOSS-01 counterfactual is the same shape one layer up: two different authority reductions producing byte-identical protected records means the record cannot answer the question it is being asked. Worth pinning as a vector regardless of where the decision envelope eventually lives.

Disclosure: drafted with AI assistance, human-reviewed, consistent with the convention already used in this thread. — Tersign

@mohammedmessaoudene-cmd

Copy link
Copy Markdown

Thanks @wowlegend for the concrete vector invitation. I pinned
tersignhq/evidence-record-conformance at
46ad663b90805a2e526ef3cd28c3f70762883125 and first ran it unchanged:
44/44 Python vectors, 44/44 TypeScript vectors, and 252 differential cases
(208 off-corpus) with zero divergence.

I then consumed the exact Git blobs for existing vectors p18, n25, and n26
through the documented AUEC adapter without changing their inputs or expected
outcomes: 3/3 passed. Based on the shape tribunal, I added a distinct
decision_evidence_binding kind with p19/n27/n28 rather than overloading the
existing position-oriented boundary kind. The contribution passes 47/47 in
Python, 47/47 in TypeScript, and 299 differential cases (252 off-corpus) with
zero divergence; a second clean-clone application and regeneration were also
byte-identical.

Draft PR: tersignhq/evidence-record-conformance#5

Its GitHub Actions run is currently action_required with zero jobs pending
first-time-contributor approval, so I am not claiming an external CI pass,
third-party review, or merge. AUEC PR #2 remains draft and unmerged:
mohammedmessaoudene-cmd/AUEC#2

Scope is structural canonical-object binding only. It does not prove producer
truth, policy correctness, historical position, MCP acceptance, or where a
future field belongs. No MCP source patch is proposed here.

AI-assistance disclosure: OpenAI ChatGPT and Codex assisted with implementation,
testing, analysis, and drafting. I reviewed the executed evidence and remain
responsible.

@Tetsurohhori

Copy link
Copy Markdown

@wowlegend — ran them. Three of them apply to what I have, and I ran all three.

boundary_binding is the only kind anchors_verify operates at, and the suite has exactly three:
n25, n26, p18. Expected and observed agree on all three. The other 41 vectors are neither
passes nor failures for me — digest recomputation, canonical bytes, chain arithmetic, independence
scope, offer binding. My verifier has no adapter path to them, and I would rather report that than
score myself on a layer I do not implement.

Both negatives reject as unattested rather than as fork or truncation. That is the property each
one isolates: n25 names its prefix truthfully and binds nothing about its own position, and n26
claims coverage through position 3 over an attestation that reaches an empty prefix. Before the fix
on 2026-08-09, n26's shape printed VERIFY OK in my tool with attested_prefix_lines=0 on the
next line. That is where the vector's description comes from, and it is my own defect, so it is
worth saying plainly that this run tests the implementation the vector was written about.

The reason this is not a blanket default-deny: p18 differs from n25 only in that its boundary
event binds its own position, and p18 is accepted. The rejection tracks the property, not a
failure to parse.

What I did not do. I did not run your harness — tools/differential.py, keccak.py, the .mjs
runners. I consumed the vector JSON at commit 46ad663 and drove my own verifier through an adapter
I wrote. That is not a comment on your code; it is that pip install-ing a checker asks the auditor
to trust a supply chain, which is the thing I have been arguing against in this thread, and it would
be inconsistent to make an exception when the code is yours. I also left p19/n27/n28 alone —
they are in a draft PR, not at the commit I pinned. Verifier diff for this run is zero lines; the
adapter is new and is mine.

Limits, stated rather than implied. No signature suite verification, so signed equivocation is
structurally out of reach for me — your framing of marking that in the artifact rather than omitting
it is the right shape, and I am not putting a date on closing it. The stream tip after the last
position-binding attestation is unattested until the next one. Force-push on the witness repository
breaks history-dependent checks. Different digest domain from the suite's keccak256/JCS core, which
is most of why 41 vectors have no path.

@mohammedmessaoudene-cmd ran the same commit before me against AUEC. Those numbers are his, not
mine, and I have not reproduced them.

On the attribution correction — noted, and thank you for making it against your own interest.

Vector bundle SHA256 (ordered file digests): ae31a62a86dffef0c917d58b004fcc579d4044af5df65aa60f91b00045906222

Disclosure: implementation, adapter, execution and drafting were assisted by AI (Claude, Cursor).
I reviewed the executed evidence and remain responsible for this comment.

@mohammedmessaoudene-cmd

Copy link
Copy Markdown

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 46ad663: p18 accepted, while n25 and n26 were classified unattested by your unchanged verifier through your own adapter.

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.

@Tetsurohhori

Copy link
Copy Markdown

@wowlegend — reciprocal, since I ran yours: I put our anchor-stream verifier up to be broken — aos-standard/catalog#1, pinned tag anchors-verify-v0.4, self-contained (the tag ships the stream and the digest sidecar, so nothing resolves against main).

Two of the three targets exist because our own verifier had those defects. It printed VERIFY OK when only part of the stream was attested, and again when the trust anchor was not pinned. The first is the same shape as n26 one layer down: a green verdict standing in for a check that was not performed. Both are now distinct outcomes with distinct exit codes, and the unattested tip is stated as out of scope rather than hidden behind a green verdict.

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.

@mohammedmessaoudene-cmd

Copy link
Copy Markdown

Follow-up on CG-DELTA-LOSS-01: the p19/n27/n28 structural decision-evidence-binding vectors have now passed external CI and were reviewed, rerun, and merged by the Tersign maintainer.

Workflow 31337102263, attempt 2, completed both jobs successfully before merge. The official logs record a 47-vector Python conformance pass, 47/47 cross-implementation agreement, 299 differential cases (252 off-corpus) with zero divergences, and byte-identical regeneration. The final merge is 79632084d94ba9841baa0f000ffd6c31ec22b3e2, tree 08890c3483b3e8dfee9e2fc9bb385414497c9112.

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.

@Tetsurohhori

Copy link
Copy Markdown

@wowlegend took the invitation I posted here on 8/10 and broke the verifier. Two findings. One of
them makes what I wrote in that comment wrong, so I am correcting it in the thread where I claimed it.

I wrote that the two defects were "now distinct outcomes with distinct exit codes". One of those
outcomes could not occur. VERIFY OK required an attested prefix containing the binding row that
states its own digest — a fixed point. I could not construct it on any route I tried, which is a
terminal state of that contract as written rather than a proof that none exists. I also wrote that
the trust anchor being unpinned was the problem. Pinning it constrained the repository name, not
whose history the named commit came from: a fork commit reachable through refs/pull/N/head is
served under the base repository's path, so a binding naming one verified with the flag set.

Both are fixed in anchors-verify-v0.7. Witness commits must now be reachable from the pin
repository's default branch (compare/{default_branch}...{sha}, ahead_by == 0), and the OK
predicate is defined over anchor record rows instead of every line. Anyone who pinned
anchors-verify-v0.4 from my comment above should move off it.

Findings and the published construction: @wowlegend. Independent offline reproduction on two
platforms: @mohammedmessaoudene-cmd. Both recorded in the conduct ledger at
aos-standard/catalog, with the detail in aos-standard/catalog#1.

Disclosure: the verifier changes, the reproduction runs, and this comment were assisted by AI
(Claude, Cursor). I ran them against the pinned tags myself and remain responsible for what is
claimed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

proposal SEP proposal without a sponsor. SEP

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

9 participants