feat(wire): carry provenance attestations on the frames envelope - #138
Merged
Conversation
There was a problem hiding this comment.
Sorry @macanderson, you've used your own review budget of 250,000 diff characters for the last 7 days.
You can request another review in 15 hours and 44 minutes by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideThis PR gives provenance attestations an additive wire representation on Sequence diagram for signing and verifying an attested frames resultsequenceDiagram
participant Provider
participant Result as ContextQueryResult
participant Host
participant Verifier
Provider->>Provider: result_set_commitments(provider_id, frames)
Provider->>Provider: result_set_root(provider_id, frames)
Provider->>Result: attach result_attestation
Provider->>Result: attach frame_attestations with optional inclusion_proof
Result-->>Host: frames result with detached evidence
Host->>Verifier: frame_commitment(provider_id, frame)
Host->>Verifier: root_from_proof(commitment, inclusion_proof)
Host->>Verifier: verify_commitment(result_attestation)
Verifier-->>Host: verified or unattested result
Flow diagram for preserving attestations during host compositionflowchart TD
Received[Receive complete signed result set]
Decide{Retain a strict subset?}
KeepAll[Keep all frames]
Derive[Derive and retain inclusion proofs]
Filter[Drop frames or deduplicate]
Verify[Verify retained evidence against the signed root]
Serve[Serve composed result]
Received --> Decide
Decide -->|No| KeepAll
Decide -->|Yes| Derive
Derive --> Filter
KeepAll --> Verify
Filter --> Verify
Verify --> Serve
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This was referenced Aug 30, 2026
macanderson
force-pushed
the
feat/wire-attestation-b726a4bc
branch
2 times, most recently
from
August 30, 2026 04:42
95e6d1f to
d4acc40
Compare
`ProvenanceAttestation` existed with nowhere to put it. A provider that signed a frame could not hand the signature to a host, so §6.5 was reachable only by out-of-band agreement between two implementations that had already met — the opposite of what a protocol is for. A `frames` result now carries `frame_attestations`, one entry per attested frame naming the `(provider_id, frame_id, content_digest)` identity it covers in full, and `result_attestation`, one signature over the Merkle root of exactly the frames returned. Both sit on the result rather than the envelope: an attestation is a property of the answer, like `truncated`, and an in-process provider that never builds an envelope must still be able to sign what it serves. Inclusion proofs are optional on the wire, and a host that keeps only part of a signed answer derives and retains them before dropping the rest — once the siblings are gone the root can never be recomputed. ADR 0014 carries the reasoning and the wire-size argument against mandating them. Every attestation this repository ships is recomputed and verified rather than asserted: a wire example whose signature nobody checks teaches an implementer to produce forgeries. Additive within `contextgraph/1` — an unsigned answer serializes to the same bytes as before and a 1.0 peer ignoring the members still parses a signed one. Closes #90 Signed-off-by: Mac Anderson <ops@oxagen.sh>
macanderson
force-pushed
the
feat/wire-attestation-b726a4bc
branch
from
August 30, 2026 05:03
d4acc40 to
b3d0f84
Compare
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull request
Summary
ProvenanceAttestation(#87) existed with nowhere to put it — a provider that signed a frame could not hand the signature to a host, soSPEC.md§6.5 was reachable only by out-of-band agreement between two implementations that had already met. This gives it a wire home, a JSON Schema, a signed example, and a test that verifies every signature this repository ships rather than trusting it.Closes #90
What changed
The wire shape — two optional members on
ContextQueryResult, theresultpayload of theframesenvelope:frame_attestations: Vec<FrameAttestation>— one entry per attested frame. Each entry is{ frame: FrameId, attestation?, inclusion_proof? }and names the(provider_id, frame_id, content_digest)identity in full. Not a parallel array indexed againstframes: position is not identity, a provider that reorders or duplicates entries would shift one frame's evidence onto another, andprovider_id/content_digestare two of the three inputs to the frame commitment and recoverable from nowhere else. This is the discipline §9'sFrameVerdictalready applies toverify.result_attestation: Option<ProvenanceAttestation>— one signature over the §6.5.3 Merkle root of exactly the frames returned.Both payload members of an entry are optional, because two honest shapes exist and neither may be unrepresentable: one root signature plus per-frame proofs (the cheap shape — one signature, not n), or per-frame signatures with no root.
They sit on the result, not the envelope. The envelope is the transport binding; it carries
typeand the correlationidand nothing else about the answer. An attestation is a property of the answer, exactly liketruncated. On the envelope it would be invisible to an in-process provider (which builds no envelope), silently dropped by the MCP bridge and MCP server (which reserialize the result into their ownstructuredContent), and would have to be redefined by any future binding.The inclusion-proof decision — optional inline, with a stated host obligation (F13). A host holding the complete set can derive every proof itself, so inlining is redundant for it — roughly
n·log₂(n)extra hashes. But a host that keeps a subset (budget truncation, cross-provider dedup, composition) cannot: the dropped siblings' commitments are gone and the root can never be recomputed again. Selective disclosure — the only reason §6.5.3 builds a tree instead of signing a list — is destroyed by ordinary composition. Mandating inline proofs taxes the common single-turn case and makes signing cost more bytes than not signing, by a margin that grows with result size; never inlining makes evidence survival depend on a host knowing to derive proofs before it filters, which nothing forces and whose omission is silent. So: a provider MAY inline them, a host that retains a strict subset MUST derive and retain them first, and a proof that is present MUST recompute the root. Full reasoning in ADR 0014.Schema —
ProvenanceAttestation,InclusionStep,InclusionProof,FrameAttestation$defs, plus the two members onContextQueryResult.Examples and vectors — a signed exchange in
examples/reference-messages.json(id: "q3") andexamples/full-stdio-session.ndjson(id: "q2"), and aframes/attestedline inschema/reference-vectors.ndjson. The example key is published inexamples/README.md; an example signature nobody can verify demonstrates nothing.Evidence, not assertion —
contextgraph-conformance/tests/attestation_wire.rsscans every wire fixture in the repo, recomputes each frame commitment, rebuilds each Merkle root, replays each inclusion proof, and verifies each signature against the published key. A wire example whose signature nobody checks teaches an implementer to produce forgeries. Side effect worth naming:contextgraph-conformancenow takescontextgraph-typeswith theattestationfeature as a dev-dependency, so the §6.5 constructions compile and their tests run under the test suite — nothing in CI did that before, and #87's whole crypto suite was dark.ci.ymlalso asks for the feature by name, because a guarantee that rests on a resolver's feature-unification detail is not a guarantee.Docs —
SPEC.md§6.5.5 + F11–F13 (appended; F1–F10 untouched), ADR 0014 with a row indocs/GUIDE.md's decision log,docs/protocol-surface.md's type snippet,CHANGELOG.mdunder[Unreleased], anddocs/adr/0010's follow-up list updated to say this landed.Mechanical churn —
ContextQueryResultgains#[derive(Default)]and 28 construction sites gain..Default::default(), so the next additive member costs no call sites.Note for #89 and #96
If #89's adversarial
--misbehavemodes need a shape to build forged attestations against, this is it:result.frame_attestations[].attestationandresult.result_attestation, withcontextgraph_types::attest::{frame_commitment, result_set_root, inclusion_proof}as the honest constructors to deviate from.attestation_wire.rs'sa_tampered_frame_is_caught_as_a_commitment_mismatch_not_a_bad_signatureis the positive-control shape a--misbehavefixture inverts. Worth reconciling at merge. #96 (record-layer hashing) touches the other attestation layer (RecordAttestation/record_hash, JCS) and should not collide.Evidence
Witness test, checked the artisanal way — by reverting the change rather than trusting the description:
query.rstest module ontoorigin/main'sContextQueryResult:error[E0560]: struct ContextQueryResult has no field named frame_attestations/result_attestation,error[E0422]: cannot find struct FrameAttestation,error[E0599]: no method named attestation_for,error[E0277]: the trait bound ContextQueryResult: Default is not satisfied. Restored →cargo test -p contextgraph-types query::→ 15 passed.origin/main.cargo test -p contextgraph-conformance --test attestation_wire→ FAILED. 4 passed; 3 failed, namingno attested 'frames' envelope found in examples/ — SPEC.md §6.5.5 is specified with nothing demonstrating it. Restored → 7 passed.Commands run:
python3 schema/validate-examples.py→OK — all examples validate(9 NDJSON lines, 13 reference messages, 17 reference vectors, 6 SPEC.md blocks, both$ids, all lifecycle fixtures)cargo test -p contextgraph-types→ 119 passed (default features)cargo test -p contextgraph-types --features attestation→ 140 + 5 + 2 + 4 passedcargo test -p contextgraph-conformance→ every suite green, 0 failedcargo test -p contextgraph-host→ 136 + 3 + 1 passedcargo test -p contextgraph-mcp-bridge -p contextgraph-mcp-server -p contextgraph-refprov→ greencargo fmt -- --check→ exit 0cargo clippy -p contextgraph-types -p contextgraph-conformance --all-targets→ exit 0, no warningsPer SCR-001 the whole-workspace suite is CI's job.
cargo checkwas run once across the workspace (exit 0) because finding every construction site of a wire type genuinely needs it.Checklist
fmt,clippy -D warnings, tests (scoped per SCR-001; see Evidence)git commit -s, DCO)CHANGELOG.mdupdated under[Unreleased]if user-visibleRegistry submission (only if adding a row to
docs/registry.md)Protocol-stability impact (if a spec/wire change)
contextgraph/1Nothing added is required. An unsigned result serializes byte-for-byte as before (
an_unsigned_answer_is_byte_identical_to_one_from_a_provider_that_predates_this), and a consumer that drops the new members still parses a signed envelope (an_old_consumer_ignoring_the_new_members_still_parses_the_envelope). No family bump. F1–F10 keep their numbers; F11–F13 are appended.License
By submitting this pull request, I agree to dual-license this contribution under MIT OR Apache-2.0, as certified by my DCO sign-off.
Update after the first CI run
clippyfailed, and the finding was real.error: using chunks_exact with a constant chunk size, incontextgraph_types::attest::from_hex— code from #87, not from this PR. It had never been reported because the whole module sits behind the off-by-defaultattestationfeature and no CI job had ever asked for it; the dev-dependency in this PR is what makes the feature compile in CI, so the lint surfaced with the change rather than being caused by it. That is the point of turning the feature on. Fixed byas_chunks::<2>(), which destructures two bytes the compiler already knows are there and rejects an odd trailing byte fromrestinstead of from a length guard someone has to keep in sync. No#[allow].It did not reproduce locally at first because this machine was on 1.97.0 and CI's
dtolnay/rust-toolchain@stableis 1.98.0. Re-verified on 1.98.0:cargo clippy --workspace --all-targets -- -D warnings→ exit 0, andcargo clippy -p contextgraph-types --all-targets --all-features -- -D warnings→ exit 0.Rebased onto
origin/mainpast #106 (SDK pins, new typecheck jobs) and #113 (cross-provider ranking). Clean, no conflicts. #113's compose path takes&ContextFrameiterators and never aContextQueryResult, so it neither constructs the new members nor drops them — consistent with #133, which tracks the host consuming attestations at all. Re-verified on the rebased tree:contextgraph-host136+13+3+1 passed,attestation_wire7 passed,reference_vectors4 passed,examples_roundtrip3 passed,python3 schema/validate-examples.py→OK — all examples validate.Sourcery: no line-level review ran — the repository's review budget is exhausted, and the check reports
skipping. Sourcery did post its Reviewer's Guide, whose "Assessment against linked issues" table is three rows, all ✅, no ❌. Read that as "no review ran", not as a clean review.Residue filed: #142 (
bridge_via_hosttimes out at default test parallelism, passes single-threaded) and #144 (the examples README's annotated session quotes lines the transcript no longer contains). Commented rather than duplicated on #133 (host consumption of the result attestation and F13's retain-before-filter obligation) and #93 (SDK port, with the shape to port).Summary by Sourcery
Carry provenance attestations alongside frames in query results and validate the complete signed wire examples.
New Features:
Enhancements:
CI:
Documentation:
Tests: