Skip to content

feat(wire): carry provenance attestations on the frames envelope - #138

Merged
macanderson merged 1 commit into
mainfrom
feat/wire-attestation-b726a4bc
Aug 30, 2026
Merged

feat(wire): carry provenance attestations on the frames envelope#138
macanderson merged 1 commit into
mainfrom
feat/wire-attestation-b726a4bc

Conversation

@macanderson

@macanderson macanderson commented Aug 30, 2026

Copy link
Copy Markdown
Owner

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, so SPEC.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, the result payload of the frames envelope:

  • 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 against frames: position is not identity, a provider that reorders or duplicates entries would shift one frame's evidence onto another, and provider_id/content_digest are two of the three inputs to the frame commitment and recoverable from nowhere else. This is the discipline §9's FrameVerdict already applies to verify.
  • 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 type and the correlation id and nothing else about the answer. An attestation is a property of the answer, exactly like truncated. 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 own structuredContent), 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.

SchemaProvenanceAttestation, InclusionStep, InclusionProof, FrameAttestation $defs, plus the two members on ContextQueryResult.

Examples and vectors — a signed exchange in examples/reference-messages.json (id: "q3") and examples/full-stdio-session.ndjson (id: "q2"), and a frames/attested line in schema/reference-vectors.ndjson. The example key is published in examples/README.md; an example signature nobody can verify demonstrates nothing.

Evidence, not assertioncontextgraph-conformance/tests/attestation_wire.rs scans 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-conformance now takes contextgraph-types with the attestation feature 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.yml also asks for the feature by name, because a guarantee that rests on a resolver's feature-unification detail is not a guarantee.

DocsSPEC.md §6.5.5 + F11–F13 (appended; F1–F10 untouched), ADR 0014 with a row in docs/GUIDE.md's decision log, docs/protocol-surface.md's type snippet, CHANGELOG.md under [Unreleased], and docs/adr/0010's follow-up list updated to say this landed.

Mechanical churnContextQueryResult gains #[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 --misbehave modes need a shape to build forged attestations against, this is it: result.frame_attestations[].attestation and result.result_attestation, with contextgraph_types::attest::{frame_commitment, result_set_root, inclusion_proof} as the honest constructors to deviate from. attestation_wire.rs's a_tampered_frame_is_caught_as_a_commitment_mismatch_not_a_bad_signature is the positive-control shape a --misbehave fixture 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:

  1. Implementation reverted, tests kept. Grafting the new query.rs test module onto origin/main's ContextQueryResult:
    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.
  2. Implementation kept, attested examples reverted to origin/main.
    cargo test -p contextgraph-conformance --test attestation_wireFAILED. 4 passed; 3 failed, naming no 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.pyOK — 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 passed
  • cargo test -p contextgraph-conformance → every suite green, 0 failed
  • cargo test -p contextgraph-host → 136 + 3 + 1 passed
  • cargo test -p contextgraph-mcp-bridge -p contextgraph-mcp-server -p contextgraph-refprov → green
  • cargo fmt -- --check → exit 0
  • cargo clippy -p contextgraph-types -p contextgraph-conformance --all-targets → exit 0, no warnings

Per SCR-001 the whole-workspace suite is CI's job. cargo check was run once across the workspace (exit 0) because finding every construction site of a wire type genuinely needs it.

Checklist

  • One logical change per PR (smaller lands faster)
  • Gate is green locally — fmt, clippy -D warnings, tests (scoped per SCR-001; see Evidence)
  • A witness test is included, or a reason there isn't one is stated below
  • Docs updated in the same PR if behavior or flags changed
  • All commits signed off (git commit -s, DCO)
  • CHANGELOG.md updated under [Unreleased] if user-visible

Registry submission (only if adding a row to docs/registry.md)

  • Not applicable — this PR does not add/change a conformance registry entry

Protocol-stability impact (if a spec/wire change)

  • Additive (new optional field/check) — safe within contextgraph/1

Nothing 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

clippy failed, and the finding was real. error: using chunks_exact with a constant chunk size, in contextgraph_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-default attestation feature 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 by as_chunks::<2>(), which destructures two bytes the compiler already knows are there and rejects an odd trailing byte from rest instead 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@stable is 1.98.0. Re-verified on 1.98.0: cargo clippy --workspace --all-targets -- -D warnings → exit 0, and cargo clippy -p contextgraph-types --all-targets --all-features -- -D warnings → exit 0.

Rebased onto origin/main past #106 (SDK pins, new typecheck jobs) and #113 (cross-provider ranking). Clean, no conflicts. #113's compose path takes &ContextFrame iterators and never a ContextQueryResult, 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-host 136+13+3+1 passed, attestation_wire 7 passed, reference_vectors 4 passed, examples_roundtrip 3 passed, python3 schema/validate-examples.pyOK — 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_host times 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:

  • Carry per-frame attestations and result-level Merkle-root attestations on frames query results.
  • Provide canonical result-set commitment and inclusion-proof support for attested frame collections.

Enhancements:

  • Define wire-level attestation requirements for frame identity binding, truncated results, and selective disclosure.
  • Add default construction support to make ContextQueryResult additions forward-compatible.
  • Verify shipped attestation fixtures by recomputing commitments, Merkle roots, inclusion proofs, and signatures.

CI:

  • Explicitly enable attestation feature coverage in CI and conformance tests.

Documentation:

  • Document attestation carriage and host proof-retention requirements in the specification, ADRs, protocol surface, examples, changelog, and schemas.

Tests:

  • Add signed attestation examples and reference vectors with conformance tests covering validity, tampering, canonical ordering, and backward compatibility.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sourcery-ai

sourcery-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR gives provenance attestations an additive wire representation on ContextQueryResult, defines canonical result-set Merkle-root and inclusion-proof behavior, updates the schema and protocol documentation, and adds signed fixtures plus cryptographic conformance tests that verify every shipped attestation.

Sequence diagram for signing and verifying an attested frames result

sequenceDiagram
    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
Loading

Flow diagram for preserving attestations during host composition

flowchart 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
Loading

File-Level Changes

Change Details Files
Add optional attestation carriers to frame query results and expose result-set commitment helpers.
  • Add frame_attestations entries keyed by full FrameId, with optional per-frame signatures and inclusion proofs.
  • Add an optional result_attestation over the canonical Merkle root of exactly the returned frames.
  • Add result inspection and matching helpers, constructors, and canonical result-set commitment/root generation behind the attestation feature.
  • Derive Default for ContextQueryResult and update construction sites for additive wire compatibility.
contextgraph-types/src/query.rs
contextgraph-types/src/attest.rs
contextgraph-types/src/lib.rs
contextgraph-conformance/src/bin/contextgraph-example-docs.rs
contextgraph-conformance/src/host_conformance.rs
contextgraph-conformance/tests/golden_fixtures.rs
contextgraph-conformance/tests/reference_vectors.rs
contextgraph-conformance/tests/stdio_roundtrip.rs
contextgraph-host/src/host.rs
contextgraph-host/src/http.rs
contextgraph-host/src/ingest.rs
contextgraph-host/src/stdio.rs
contextgraph-host/src/wire.rs
contextgraph-mcp-bridge/src/lib.rs
contextgraph-mcp-server/src/lib.rs
contextgraph-refprov/src/lib.rs
Define and document the wire contract for attestations, including placement, identity binding, Merkle roots, and proof-retention obligations.
  • Add schema definitions for provenance attestations, inclusion proofs, and frame attestations, plus optional result fields.
  • Specify F11–F13 and add the dedicated §6.5.5 wire guidance.
  • Record the design decisions and protocol surface, including result placement and optional inline proofs with host obligations.
  • Update changelog and prior ADR follow-up documentation.
SPEC.md
schema/contextgraph-envelope.schema.json
docs/adr/0014-attestations-on-the-wire.md
docs/GUIDE.md
docs/adr/0010-provenance-attestation.md
docs/protocol-surface.md
CHANGELOG.md
Ship verifiable signed wire fixtures and enforce cryptographic correctness across repository examples.
  • Add signed frames exchanges and an attested reference vector, including both per-frame and root-only evidence shapes.
  • Publish the deterministic example signing key and explain how to verify fixture signatures.
  • Scan all attested fixtures and recompute commitments, roots, inclusion proofs, and signatures, including tamper and canonical-order checks.
  • Make attestation-feature tests explicit in CI and enable the feature for conformance test development.
contextgraph-conformance/tests/attestation_wire.rs
contextgraph-conformance/tests/reference_vectors.rs
contextgraph-conformance/Cargo.toml
.github/workflows/ci.yml
examples/README.md
examples/full-stdio-session.ndjson
examples/reference-messages.json
schema/reference-vectors.ndjson
Extend fixture validation to recognize signature placeholders.
  • Allow schema example validation to substitute 128-hex signature placeholders.
schema/validate-examples.py

Assessment against linked issues

Issue Objective Addressed Explanation
#90 Add detached per-frame and result-set attestation fields to the frames result, including full FrameId identities, representable RFC 6962 Merkle-root attestations, and an explicit inclusion-proof policy.
#90 Define and document the wire format and compatibility behavior, including JSON Schema support, normative SPEC requirements, ADR decision records, protocol-surface updates, changelog entry, and additive compatibility within contextgraph/1.
#90 Provide signed wire examples and conformance tests that cryptographically verify shipped frame attestations, Merkle roots, and inclusion proofs while covering detached placement, serialization compatibility, and example/schema validation.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

`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
macanderson force-pushed the feat/wire-attestation-b726a4bc branch from d4acc40 to b3d0f84 Compare August 30, 2026 05:03
@macanderson
macanderson merged commit 75aa33d into main Aug 30, 2026
29 checks passed
@macanderson
macanderson deleted the feat/wire-attestation-b726a4bc branch August 30, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Carry ProvenanceAttestation on the wire (frames envelope + JSON Schema)

1 participant