§6.3 makes provider id one third of every frame's stable identity and never says which provider id it is. There are two, they differ, and the reference implementation puts a different one in each of the three places the triple is used — including on the wire.
In plain words
Every retrieved piece of content ("frame") is supposed to get one stable identity, and a third of that identity is a "provider id." There are actually two different things that phrase could mean — the name an operator privately assigned a provider, versus the name the provider announces about itself — and they aren't always the same value. The reference code uses one meaning in some places (like matching evidence for audits) and the other meaning elsewhere (like the cryptographic attestation), including sending the wrong one over the wire to the provider itself. This ambiguity already caused a real bug where honest, correctly-signed evidence was mistakenly flagged as tampered.
The two ids:
- The host-local id — the routing and consent key the operator chose (
Host::add_stdio(id, …)). §6.5.2 notes it "is not a string the provider ever sees".
- The handshake-declared name —
provider.name from §3, which §6.5.2 calls "the only identifier both ends of the wire observe".
§6.3 says only: "A frame's stable identity is the triple (provider id, frame id, content_digest)." §9's verify request carries that triple. §7.3's UR1 says a billed total must be "walkable back to the exact (provider id, frame id, content_digest) triples behind it". §14's A1 makes the same triple the attribution handle. None of them disambiguates.
Where the reference implementation lands, per surface
| Surface |
id used |
Source |
FrameId in composition, dedup, audit, usage reports |
local |
host.rs:588 — outcome.provider_id |
| Attestation commitment |
declared |
§6.5.2, contextgraph-example-docs.rs:455 |
verify request sent to the provider |
local |
host.rs:201 groups by FrameId::provider_id |
The third row is the one that reaches the wire. Host::verify_frames sends the provider a FrameId whose provider_id is a string the provider has never seen and cannot recognise. It works today only because the reference provider ignores the field entirely — verify_honestly (contextgraph-example-docs.rs:727) matches on frame_id alone. A provider that took §9's "echoes the identity it answers in full" at its word and validated provider_id against its own name would reject every verify request the reference host sends, and would be right to.
This same ambiguity produced a live defect in the attestation path, fixed separately: the host recomputed the commitment with its local id and reported CommitmentMismatch — the tampering verdict — for honest signatures. The fixture had documented the distinction at contextgraph-example-docs.rs:448 and the host simply did not honour it. That one is patched; the spec ambiguity that allowed it is not.
The second-order problem: identities that leave one host
Inside one host either choice is self-consistent. The triple stops being self-consistent the moment it travels, which is exactly what docs/context-reuse.md and UR1 are for:
- Local id ⇒ the triple is meaningless to anyone else. Two hosts that name the same provider differently compute different identities for identical evidence, so cross-host dedup, a shared cache, and a warehouse rolling up usage across fleets all silently fail to match.
- Declared name ⇒ the triple is attacker-controlled. H2 requires only that
provider.name be non-empty; nothing requires it to be unique, registered, or verified. A hostile provider declaring name: "repo-graph" mints frame identities that collide with the honest repo-graph's, poisoning dedup (D2 says same triple ⇒ same content), reuse (D3), and the attribution/usage join (A1, UR1).
So the field is either not portable or not trustworthy, and the spec has not said which — while D2 states a MUST about how hosts treat two frames that share it.
Reproduce
rg -n 'provider_id' contextgraph-host/src/host.rs | head
rg -n 'attestation_provider_id' contextgraph-conformance/src/bin/contextgraph-example-docs.rs
rg -n 'fn verify_honestly' -A 20 contextgraph-conformance/src/bin/contextgraph-example-docs.rs
Register the reference provider under any local id other than its declared name (the suite uses provider-under-test), then compare the provider_id in the verify request on the wire against the one in the frame commitment. They differ.
Why this needs a decision rather than a patch
Which id belongs in the triple is a protocol-semantics decision with a different answer depending on what the triple is for, and the answer constrains three separate things already shipped: verify's wire shape, the usage-report join, and the attestation preimage (which is frozen — §6.5.2 already names the declared name, and changing it is a new major family). The plausible resolutions are not equivalent:
- Declare it the handshake name everywhere, and add the uniqueness/verification story H2 lacks — possibly a namespaced or operator-confirmed name.
- Declare it the local id everywhere, and accept the triple is host-scoped — which contradicts §6.5.2 and makes UR1's cross-fleet story much weaker.
- Say the triple carries the declared name on the wire and the local id internally, and specify the mapping — which is roughly what the code now does, but deliberately and in writing.
Pillar
Reliability and stability. D2 is a MUST about content equality keyed on a field whose referent is undefined; UR1 and A1 build billing and attribution on the same key.
Done looks like
- §6.3 states which id
provider id names, and says what a host does with the other one.
- §9 states which id a
verify request carries, and whether a provider may reject a request that names an id it does not recognise.
- If the answer is the declared name: a rule constraining it beyond H2's non-empty, and a statement of what a host does when two configured providers declare the same one.
- A conformance check, or a host-conformance scenario, that registers a provider under a local id differing from its declared name and asserts every triple-consuming path still agrees — the case no existing test covers, which is why three surfaces diverged unnoticed.
- Lands in the crate that already owns the divergent behaviour (
contextgraph-host, plus the conformance fixtures), without introducing a new god file under this workspace's size conventions.
- Any new SPEC.md prose cites code by symbol (
Host::verify_frames, FrameId::provider_id) rather than a pinned line number, matching this org's citation convention.
§6.3 makes
provider idone third of every frame's stable identity and never says which provider id it is. There are two, they differ, and the reference implementation puts a different one in each of the three places the triple is used — including on the wire.In plain words
Every retrieved piece of content ("frame") is supposed to get one stable identity, and a third of that identity is a "provider id." There are actually two different things that phrase could mean — the name an operator privately assigned a provider, versus the name the provider announces about itself — and they aren't always the same value. The reference code uses one meaning in some places (like matching evidence for audits) and the other meaning elsewhere (like the cryptographic attestation), including sending the wrong one over the wire to the provider itself. This ambiguity already caused a real bug where honest, correctly-signed evidence was mistakenly flagged as tampered.
The two ids:
Host::add_stdio(id, …)). §6.5.2 notes it "is not a string the provider ever sees".provider.namefrom §3, which §6.5.2 calls "the only identifier both ends of the wire observe".§6.3 says only: "A frame's stable identity is the triple (provider id, frame id,
content_digest)." §9'sverifyrequest carries that triple. §7.3's UR1 says a billed total must be "walkable back to the exact (provider id, frame id, content_digest) triples behind it". §14's A1 makes the same triple the attribution handle. None of them disambiguates.Where the reference implementation lands, per surface
FrameIdin composition, dedup, audit, usage reportshost.rs:588—outcome.provider_idcontextgraph-example-docs.rs:455verifyrequest sent to the providerhost.rs:201groups byFrameId::provider_idThe third row is the one that reaches the wire.
Host::verify_framessends the provider aFrameIdwhoseprovider_idis a string the provider has never seen and cannot recognise. It works today only because the reference provider ignores the field entirely —verify_honestly(contextgraph-example-docs.rs:727) matches onframe_idalone. A provider that took §9's "echoes the identity it answers in full" at its word and validatedprovider_idagainst its own name would reject every verify request the reference host sends, and would be right to.This same ambiguity produced a live defect in the attestation path, fixed separately: the host recomputed the commitment with its local id and reported
CommitmentMismatch— the tampering verdict — for honest signatures. The fixture had documented the distinction atcontextgraph-example-docs.rs:448and the host simply did not honour it. That one is patched; the spec ambiguity that allowed it is not.The second-order problem: identities that leave one host
Inside one host either choice is self-consistent. The triple stops being self-consistent the moment it travels, which is exactly what
docs/context-reuse.mdand UR1 are for:provider.namebe non-empty; nothing requires it to be unique, registered, or verified. A hostile provider declaringname: "repo-graph"mints frame identities that collide with the honestrepo-graph's, poisoning dedup (D2 says same triple ⇒ same content), reuse (D3), and the attribution/usage join (A1, UR1).So the field is either not portable or not trustworthy, and the spec has not said which — while D2 states a MUST about how hosts treat two frames that share it.
Reproduce
Register the reference provider under any local id other than its declared name (the suite uses
provider-under-test), then compare theprovider_idin theverifyrequest on the wire against the one in the frame commitment. They differ.Why this needs a decision rather than a patch
Which id belongs in the triple is a protocol-semantics decision with a different answer depending on what the triple is for, and the answer constrains three separate things already shipped:
verify's wire shape, the usage-report join, and the attestation preimage (which is frozen — §6.5.2 already names the declared name, and changing it is a new major family). The plausible resolutions are not equivalent:Pillar
Reliability and stability. D2 is a MUST about content equality keyed on a field whose referent is undefined; UR1 and A1 build billing and attribution on the same key.
Done looks like
provider idnames, and says what a host does with the other one.verifyrequest carries, and whether a provider may reject a request that names an id it does not recognise.contextgraph-host, plus the conformance fixtures), without introducing a new god file under this workspace's size conventions.Host::verify_frames,FrameId::provider_id) rather than a pinned line number, matching this org's citation convention.