feat: context/verify — pull-based frame revalidation - #38
Conversation
The request/response shapes for pull-based frame revalidation (#26, docs/context-reuse.md §4): - `VerifyRequest` carries `FrameId` identities only — never frame bodies. Verification costs bytes, not tokens. - `Verdict`: valid | stale{replacement_digest?} | gone | unknown, internally tagged on `status` so it is self-describing and extensible. A stale verdict may offer the provider's current digest — a digest, never a body. - `FrameVerdict` echoes the full identity, so a host correlates by matching rather than trusting positional alignment. - `Verdict::permits_reuse()` is true only for `valid`: reuse requires a positive answer, never the absence of a negative one. `warrants_requery()` is false for `gone` — nothing there to re-fetch. - `Capabilities.verify` defaults false, so a pre-§4 provider is treated as unsupported and the host falls back to re-query. Independent of `subscribe` (#6): push and pull freshness are complementary axes. Zero new dependencies. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
…honesty check - Envelope::Verify / Envelope::Verified carry the request/response; stdio and HTTP transports dispatch them exactly as query/frames. - ContextProvider::verify defaults to answering `unknown` for every identity, so no existing provider must implement anything — the host then re-queries. - Host::verify_frames groups held identities by provider, asks each capable provider once, and partitions into retained vs dropped. Default-deny: a frame is retained only on an explicit `valid`. Every other outcome drops it with a DropReason (stale/gone/unknown/no-digest/verify-unsupported/unknown-provider/ verify-failed), and DropReason::warrants_requery is false only for `gone`. Verdicts are correlated by full identity, never by position; a missing verdict is `unknown`, since silence is not validity. Per-provider failures are isolated like query legs — a verify timeout drops that provider's frames, never another's. The host holds no frame cache and no turn state. - verify-honesty conformance check: asks twice about frames the provider just served — once with real digests (must be `valid`), once with mutated digests (must be `stale`). A mutated digest is what a changed source looks like from the provider's side, so honest staleness detection is testable without mutating a real source. Skipped, not failed, when `verify` is unadvertised — that is the declared fallback. - Two new fixture modes prove the check bites from both directions: --misbehave rubber-stamp-verify (always `valid`) and hollow-verify (advertises the capability, vouches for nothing). Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
Ten tests pinning the §4 host contract: stale and gone frames are dropped while a valid one is retained; an unknown verdict AND a missing verdict both drop the frame (silence is not validity); a provider without the verify capability is never asked at all and falls back to re-query; a digest-less frame never reaches the wire; one provider's verify failure drops only its own frames; frames from an unregistered provider are dropped rather than silently ignored; a provider's held frames are batched into one request; the retained/dropped partition is total, so no held identity is ever lost. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
Keeps schema/ and examples/ in lockstep with the types, per the repo convention: - schema: Verify/Verified envelope variants, FrameId, VerifyRequest, VerdictStatus, FrameVerdict, VerifyResponse, and the optional Capabilities.verify flag. FrameVerdict is a flat object rather than an allOf composition — allOf against a closed Verdict subschema rejects the flattened `frame` field, which is exactly what the example caught. - examples/reference-messages.json: the served frames now carry a content_digest (so they are verifiable at all), the repo-graph provider advertises verify, and a full verify -> verified exchange is documented in which one frame is valid and one is stale with a replacement digest. - tests/verify_wire.rs: the published messages must parse as envelopes and the verify exchange must round-trip byte-for-byte; a verify envelope must carry no frame-body field; and the real stdio fixture is driven end-to-end (valid / stale-with-current-digest / gone), with the host demonstrably evicting the stale frame and re-querying only what is worth re-fetching. Verified: all 9 reference messages validate against the JSON schema (jsonschema Draft202012Validator). Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
- context-reuse.md §4 (the anchor and the D1/D4 cross-references were already reserved for it): the problem, the exchange, why no frame body travels in either direction, digest-as-ground-truth, and the default-deny host rule. Adds the verify-vs-subscribe comparison the issue asked for, so the 1.0 freeze can keep both, either, or one — both are capability-gated and default false, so there is no flag day in any of those directions. Conformance rows V1-V4. - protocol-surface.md: the verify types, the `verify` capability flag, the verify/verified envelope variants, and V1-V4 replacing the two forward-looking placeholder rows that named a check that did not exist yet. - implementing-a-provider.md: the two new envelopes in the vocabulary table and a step explaining how to answer honestly — and that leaving `verify` unset costs a provider nothing. Verified: python3 schema/validate-examples.py passes on all 14 bundled example messages; fmt/clippy/test all green (92 tests). Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
There was a problem hiding this comment.
Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
- full-stdio-session.ndjson is billed as a complete session, so it now shows the verify/verified exchange too: the provider advertises verify, its served frames carry content_digests (without which nothing is verifiable at all), and one frame comes back valid while the other is stale with a replacement digest. reference-messages.json already documented the shape; this makes the end-to-end transcript match. - context-reuse.md §4: explain why the freshness window is host-tolerated rather than provider-declared — only the host knows how much staleness a given task absorbs, and a wire field would push a scheduling decision into the protocol and give the host state to keep. Notes that a provider-supplied freshness *hint* is available later as an additive capability field, which would inform host policy without overriding it. Verified: python3 schema/validate-examples.py passes on all 16 messages. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
|
Independently verified locally by the orchestrating session (this repo has no GitHub Actions workflows, so local gates are the only gate):
Also confirmed the implementation is real code rather than spec prose: |
Signed-off-by: Mac Anderson <mac@oxagen.sh>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ts signature requires 4 (`caps`), causing an E0061 compile error.
This commit fixes the issue reported at contextgraph-conformance/src/lib.rs:116
## Bug
In `contextgraph-conformance/src/lib.rs`, the merge commit `1197789` reintroduced the old 3-argument call site from `main` while keeping the new 4-parameter signature.
Call site (line 116):
```rust
run_query_and_shutdown_checks(host, &id, &mut checks).await;
```
Signature (line ~215):
```rust
async fn run_query_and_shutdown_checks(
host: Host,
id: &str,
caps: &Capabilities,
checks: &mut Vec<CheckResult>,
) { ... }
```
This is a hard compile error (`E0061`: this function takes 4 arguments but 3 were supplied). The crate cannot build. `caps` is already in scope from the `Ok((host, id, info, caps))` match arm, so the fix is straightforward.
## Fix
- Restored the 4-arg call: `run_query_and_shutdown_checks(host, &id, &caps, &mut checks).await;`
- Secondary merge artifact: the handshake-failure skip list contained `CHECK_FRAME_VALIDITY` twice, which would push two skip entries for the same check into the report. De-duplicated to a single `CHECK_FRAME_VALIDITY`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: macanderson <mac@macanderson.com>
Reconcile the pre-freeze normative sweep (#33) with frame representations (#41, full/compact/reference) and context/verify (#38), which landed on main after this branch forked. Key reconciliations: - Capabilities: keep #33's ADR-0004 removal of upsert/subscribe/filters and its `correlation` field; add #41's `verify`/`representations`/`resolve`. - ContextFrame: adopt main's optional `content` + representation model (representation/content_ref/canonical_content_hash/…); rename #33's `canonical_token_cost()` method to `expected_inline_token_cost()` to free the name for #41's `canonical_token_cost` field, and make it handle absent content. - wire::Envelope: keep #33's correlation `id` on query/frames and structured `error` (id/code) AND #41's Verify/Verified variants. - Conformance fixture: keep #33's strict frame-building (valid F5 digests, F4 timestamps) so its enhanced checks pass, grafted with main's verify (verify_honestly/current_digest derived from the fixture's own digests). - schema/examples: union of #33's stricter validations (timestamp/embedding patterns) and #41's representation $defs; drop the removed capability fields. Verified green: cargo build/clippy -D warnings/test, schema validate-examples, and conformance-red.sh (all 14 misbehave modes caught, integrating both #33's and #41's). The frame-representations witness (previously #[ignore]) now passes against main's implementation.
…top-10 build) (#60) * docs(spec): normative completeness pass for the freeze (#49, #50, #48, #13) Fold every shipped wire surface into SPEC.md, the single normative home, and restore the consent-scopes text a prior merge dropped. - §9 Verification (verify/verified, V1-V4) - §6.3 Frame identity (D1-D4); §6.4 Representations (P1-P5) - §6.4.1: context/resolve scoped OUT of 1.0 (deferred to a 1.x additive minor; docs/sketches/resolve.md). Remote providers should not emit un-rehydratable reference frames — closes the dead-capability surface (#50). - §4.1 egress scopes + consent receipts (C5-C6); restored context-reuse.md §3 (recovered from d229ed9, the text the #38 merge destroyed) - §4.2 transport security: C7 (TLS for non-loopback), C8 (no credential logging) (#13) - §13 Extensibility: U1-U4 (ignore-unknown, closed FrameKind / open vocabularies, reserved : namespaces, no-repurpose/deprecation) — schema is authoring-strict, U1 is the interop contract (#48) - §10 error codes: unsupported_representation, incompatible_version No wire-shape change: documents surfaces the schema and reference types already carry. Schema examples validate; contextgraph-conformance green. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * docs(schema): record authoring-strict vs interop profile ($48 schema half) additionalProperties:false stays as an authoring/CI lint; the interop contract is SPEC.md §13 U1 (receivers MUST ignore unknown members). No behavioral change, so downstream drift gates that pin this schema keep working. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * docs(profile): draft Context Exchange Provider profile skeleton (#28) Frame the CEP profile that layers the deferred write-path (#5), resolve (#50), retention, and attestation on top of core contextgraph/1.0 — the concrete path to a second independent implementation (freeze criterion 1). Summarizes the in-progress Oxagen reference implementation and marks every unresolved contract decision [OPEN]. Explicitly a draft skeleton, not a normative profile. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * docs(surface): sync protocol-surface with shipped types + defer authority to SPEC.md (#49, #21) - Fix stale capability structs: drop removed upsert/subscribe/filters, add the shipped correlation/representations/resolve; relabel DataFlow.writes as the consent-surface declaration ADR 0004 made it; add representation_preferences. - Reconcile the triple-claimed authority: protocol-surface.md's conformance table is now an index that MIRRORS SPEC.md (normative on conflict); GOVERNANCE points at SPEC.md as the conformance home. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * docs(publishing): de-stella the crates.io release checklist (#16, #30) Remove the dangling RELEASING.md link and stella write-access / tag-namespace references — this repo publishes the three crates independently. Crate metadata verified publish-ready (contextgraph-types `cargo publish --dry-run` packages clean). NOT published and NOT tagged here: that is an irreversible, rights-gated step for a maintainer to run from this checklist. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * feat(conformance): close post-#41 enforcement residue (#51) Four enforcement gaps the shipped surface opened, each with an adversarial witness (conformance-red.sh: all 17 misbehave modes caught). - P1-P3 representation invariants: check_frames now calls representation_invariants() on every frame; witness `lying-representation` (reference frame with inline content). - F4 as_of probe: new `as-of-temporal` check fires an as_of-pinned query and fails frames whose valid_from is after the pin; fixture frames given disjoint validity windows; witness `ignore-as-of`. - B4 host-side max_frames audit: ProviderResult::FrameFlood, respects_frame_limit, frame_floods() — drops the leg loudly, symmetric to the B2 budget-lie drop. - E1 embedding fingerprint: reference provider declares a fingerprint and replies bad_request to a wrong-dimension vector; new stdio raw-wire `embedding-fingerprint` check; witness `accept-bad-embedding`. Documented stdio-only limitation. cargo test -p contextgraph-conformance -p contextgraph-host green (13 + 71); cargo test --workspace green; conformance-red.sh: "All misbehaviour modes caught." Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * feat(host): end-to-end provenance digest verification (#12) The remaining half of F5: the provider-facing suite checks digest grammar; a host re-reads the source bytes and checks they hash to the declared digest. - contextgraph-host/src/verify.rs: verify_provenance_digest / verify_file_provenance returning DigestVerification { Verified | Mismatch{expected,actual} | Unreadable{reason} | NotFileProvenance }. Reads the exact bytes addressed by uri+range per SPEC §6.2 — no normalization (no line-ending translation, no \r strip, no trailing-newline adjustment); no range = whole resource; line ranges L<s>-<e> inclusive; unknown range grammar => Unreadable (never silent fallback); file:// only, percent-decoded, non-local host rejected. - 11 tests incl. known-answer sha256 vectors (interop, not just self-consistency), CRLF no-normalization, sub-range, missing file. - Deliberately NOT auto-wired into query_all: re-reading a provider-named uri is a confinement/consent decision left to the host-side harness (#14). SPEC §11.1 updated. cargo test -p contextgraph-host green (82); cargo fmt clean. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * test(fixtures): B3-honest goldens + compact/reference vectors (#52) The golden set downstream consumers copy + drift-gate predated #33/#41. - Fix B3 dishonesty flagged in the issue: context-frame.valid.json declared token_cost 42 (content is 49 bytes => ceil(49/4)=13) and 0 (39 bytes => 10). Corrected; the golden test never caught it because it runs check_frames, not check_budget. Regenerated the embedded JCS + per-case sha256. - Add representation goldens: context-frame.compact.valid.json (all 5 required fields, B3-honest token_cost 28) and context-frame.reference.valid.json (no inline content, token_cost 0). Real sha256 digests over the actual bytes. - Regenerate manifest.json (7 entries, correct on-disk hashes). - golden_fixtures.rs: extend FIXTURE_FILES 5->7 and add a test asserting the two vectors satisfy representation_invariants + B3. Note: FRAME_FIELDS strict allow-list excludes content_digest by a pre-existing comment, so compact frames can't pass strict_frame; representation vectors are validated as real ContextFrames instead. Whether to widen that frozen allow-list is left for the maintainer. cargo test -p contextgraph-conformance green (golden_fixtures 8); workspace green. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * fix(conformance): collapse if-let to let-chains for the clippy -D warnings gate (#51) The #51 misbehave dispatch used nested `if cond { if let Some(x) = .. }`, which `cargo clippy --workspace --all-targets -- -D warnings` (CI's gate) flags as collapsible_if under MSRV 1.90 (let-chains stable since 1.88). Collapsed to `if cond && let Some(x) = ..`; behavior-preserving (identical short-circuit, no else). conformance-red.sh re-run: all misbehave modes still caught. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * feat(conformance): host-side conformance harness — "conformant host" is checkable (#14) Makes GOVERNANCE freeze criterion 4 real for the host-binding rules. Drives the reference host against adversarial in-process providers (the host-side analog of --misbehave), each check asserting the host CATCHES a bad provider AND ACCEPTS a good one (non-vacuous in both directions). - host-budget-drop (B2): over-budget frames dropped-with-report. - host-frame-limit (B4): >max_frames flood dropped-with-report. - host-consent-gate (C1/C2): unconsented egress refused; ProbeProvider's queried flag proves the payload was never transmitted; queried+accepted after consent. - host-scope-receipt (C6): unreceipted off-machine scope refused with the typed scope error, payload not transmitted; accepted after a receipt. - host-provenance-bytes (F5-bytes): wires #12's verify_file_provenance over a harness-owned temp file; tampered digest caught as Mismatch, matching Verified. - host-content-quoting (R3): content fenced as quoted material. Honestly scoped: checks the delimiting contract, NOT breakout-resistance (issue #15). Surfaced via `contextgraph-inspect host [--json]` and .github/scripts/host-conformance.sh (host-side red-detection analog), wired into CI. SPEC §11.1 rewritten: B2/B4/C1/C2/ C6/F5-bytes/R3-delimiting now checked; honest residual = C4/C7/C8 transport (need a TLS/network peer), R3-breakout (#15), F5 provider-path confinement. cargo clippy --workspace --all-targets -D warnings clean; cargo test --workspace green (21 groups); host-conformance.sh: all 6 PASS; conformance-red.sh: all caught. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw * fix(sdk): declare embeddings_fingerprint + reject wrong-dim embeddings in example providers (#51) The new embedding-fingerprint (§E1) conformance check skips a provider that declares no fingerprint, and conformance-external.sh (like conformance-green.sh) treats a skip as non-conformant — a deliberate, symmetric "every check green, none skipped" bar. #51 updated the Rust reference provider to declare a fingerprint and reject wrong-dimension embeddings; the three SDK example providers now mirror it so they pass the same bar (rather than relaxing it). - Each example provider declares embeddings_fingerprint "bge-small-en-v1.5/384/l2" and replies error{code: bad_request} to a query embedding whose length != 384. - Small runtime additions so an example can signal a coded protocol error: Python ProviderError, Go ProviderError + (result, error) Query signature + the missing ContextQuery.Embedding field, TypeScript ProviderError. Verified locally against the real CI steps: python / go (vet+build) / ts (tsc) each `All 9 checks passed — external provider is conformant`, embedding-fingerprint OK. Claude-Session: https://claude.ai/code/session_01NGCCkBsqCdJiNvFBHQKgyw
Implements #26 from scratch. The issue was auto-closed by PR #32's keyword with no implementation landed — only design prose in
docs/context-reuse.md. There was zeroverifycode in anycontextgraph-typesorcontextgraph-hostsource file; the#4-context-verificationanchor, theverifycapability mentioned in the spec-version note, and the D1/D4 cross-references all pointed at a section that did not exist. This builds it.The exchange
The host sends the frame identities it holds; the provider answers one verdict each.
No frame body travels in either direction — the economic point of the whole feature, and it is asserted against the serialized envelope rather than merely documented (
verify_wire.rs). Evenstalecarries at most the provider's current digest: enough to tell a host what it would be re-fetching, never the replacement content.Built directly on
identity.rsfrom #23 —FrameIdis the verify unit and its existingis_verifiableis the digest gate. No parallel identity type was minted.Correctness choices worth reviewing
Verdicts echo the full identity.
FrameVerdictcarries theFrameIdit answers rather than relying on array position, so a provider that reorders, omits, or duplicates entries cannot shift avalidonto the wrong frame. Since the digest is part of the identity, a verdict about a different digest does not answer for the one asked — there is a test pinning exactly that.Default-deny. A frame is retained only on an explicit
valid. An identity that comes back with no verdict at all is treated asunknown, not as tacit approval — silence is not validity.Host::verify_framesreturns a total partition of its input intoretained+dropped, and a test asserts no held identity can be silently lost.goneis the one reason that doesn't warrant a re-query — the frame is not there to re-fetch. Everything else (stale, unknown, no digest, unsupported, failed) drops the frame and marks it worth fetching again.The host stays stateless.
verify_framescaches no frames and tracks no turn boundaries. "Re-verify past a freshness window" is written as informative host guidance in the spec, not built as machinery — a turn-boundary cache is subscribe's lifecycle, and building one here would blur the exact line §4 exists to draw.Capability gating and fallback
Capabilities.verifydefaults tofalse, so a pre-§4 provider is treated as unsupported and its frames are re-queried. The host never even sends a request to such a provider (asserted, not assumed).ContextProvider::verifydefaults to answeringunknownfor everything, so a provider that implements nothing can never accidentally bless a stale frame. Verify failures are isolated exactly like query fan-out legs — one provider's timeout drops its own frames and never another's.Conformance: honest staleness without mutating a real source
The subtle part. Because the digest is provider-declared and opaque (§1), the suite cannot check the provider's answer — only that it distinguishes. So
verify-honestyasks twice about frames the provider just served:valid;stale.That is the issue's "provider answers honestly for a mutated source (digest mismatch ⇒ stale)" requirement, testable against any provider without the suite needing write access to anyone's sources.
Two fixture modes prove the check bites in both directions:
--misbehave rubber-stamp-verify— always answersvalid; fails ask 2. This is the dangerous lie: it would let a host go on citing evidence that changed.--misbehave hollow-verify— advertises the capability but answersunknownto everything; fails ask 1.The check is skipped, not failed, when a provider doesn't advertise
verify— that is the declared fallback, not a violation.verify_wire.rsadditionally drives the real stdio fixture end-to-end over an actual pipe: a served frame verifiesvalid, a mutated digest comes backstalecarrying the current digest and is demonstrably evicted by the host, and an unserved frame comes backgoneand is correctly not queued for re-query.Verify vs subscribe (#6) for the freeze
docs/context-reuse.md§4 includes the comparison the issue asked for: who initiates, transport needs, detection latency, cost shape, and which providers each fits. They are complementary, not alternatives — push has no answer for a host that reconnects and wants to know whether its cache is still good; pull has no answer for a host needing millisecond invalidation. Both flags are independent and defaultfalse, so the freeze can keep both, either, or one with no flag day. I did not implement any of #6's scope.Verification
Schema and examples are kept in lockstep with the types per the repo convention, and
verify_wire.rsmakes that a test rather than a habit: the published reference messages must parse as envelopes and the verify exchange must round-trip byte-for-byte. That test is how I caught theFrameVerdictschema def being wrong —allOfagainst a closedVerdictsubschema rejects the flattenedframefield.Notes for adjacent issues
HostError. Verdicts are normal responses, and capability-gating removes any "unsupported" error case, so there is no code for P1: Structured error codes on the wire #9 to reconcile.wire.rs,capability.rs, the conformance suite, schema, and docs. I read them rather than editing their files, so textual conflicts on those shared files are expected; none of the semantics overlap. Pre-freeze normative sweep: SPEC.md, canonical token accounting, conformance that actually catches things #33 addserror_code.rsandSPEC.md, which I stayed out of entirely.docs/context-reuse.md§3 and its own conformance check, so both PRs grow the conformance count and the same doc file. Adjacent sections, no semantic conflict.Closes #26