Skip to main content

contextgraph_host/
provider.rs

1//! The uniform provider handle.
2//!
3//! Every context source — an in-process built-in, a child process over
4//! stdio, or a remote HTTP endpoint — reaches the host through one trait,
5//! [`ContextProvider`]. Its shape mirrors the two Context Graph Protocol methods a host always
6//! needs: capability negotiation (cached from the handshake, SPEC.md §3) and
7//! `context/query` (SPEC.md §5). `stella-context` and any other Rust agent drive
8//! all three provider kinds through this single interface
9//! (`SPEC.md` §1 — "usable by any other Rust agent that wants Context Graph Protocol
10//! support").
11
12use async_trait::async_trait;
13use contextgraph_types::{
14    Capabilities, ContextQuery, ContextQueryResult, FrameKind, ProviderInfo, Verdict,
15    VerifyRequest, VerifyResponse,
16};
17
18use crate::error::HostError;
19
20/// A registered Context Graph Protocol provider, queryable behind one handle regardless of
21/// transport. `info()`/`capabilities()` return values cached at handshake
22/// time, so they are cheap synchronous getters even for out-of-process
23/// providers.
24#[async_trait]
25pub trait ContextProvider: Send + Sync {
26    /// The provider's host-facing id — its routing key and its consent key
27    /// (`SPEC.md` §4 and §10).
28    fn id(&self) -> &str;
29
30    /// Identity + declared data-flow direction, surfaced at consent time
31    /// (SPEC.md §3, ).
32    fn info(&self) -> &ProviderInfo;
33
34    /// Capabilities negotiated at the handshake (`SPEC.md` §3): which frame
35    /// kinds this provider serves, whether it echoes a correlation `id`, does
36    /// graph, names an embedding space, answers `context/verify`, which frame
37    /// representations it can return, and whether it answers `context/resolve`.
38    ///
39    /// That is the whole of [`Capabilities`] — seven fields. This comment used
40    /// to describe `upsert`, `subscriptions` and `filters`, which
41    /// [ADR 0004](https://github.com/macanderson/context-graph-protocol/blob/main/docs/adr/0004-dead-capability-surface.md)
42    /// removed because nothing implemented them. The sentence outlived them and
43    /// was copied into `docs/implementing-a-provider.md`, so a provider author
44    /// read it as the contract (#151).
45    fn capabilities(&self) -> &Capabilities;
46
47    /// Answer a context query with budgeted, provenance-carrying frames
48    /// (SPEC.md §5). The host — not the provider — enforces the budget and consent;
49    /// a provider that over-runs its budget is caught by the host, not
50    /// trusted (`crate::host`).
51    ///
52    /// # Signing
53    ///
54    /// A provider that signs what it serves populates
55    /// [`frame_attestations`](ContextQueryResult::frame_attestations) and
56    /// [`result_attestation`](ContextQueryResult::result_attestation) on the
57    /// result it returns (`SPEC.md` §6.5.5). There is no second method and no
58    /// second channel: the evidence is part of the answer, so an in-process
59    /// provider and a transport-backed one carry it identically, and a host
60    /// can never be handed signatures that disagree with the frames they cover
61    /// (ADR 0014).
62    ///
63    /// This trait previously offered a defaulted `query_attested` returning an
64    /// `AttestedQueryResult`, from the months when the `frames` envelope had
65    /// nowhere to put an attestation. It does now, so both are gone.
66    ///
67    /// The host checks whatever arrives against its
68    /// [`TrustStore`](crate::TrustStore) and records the outcome in the
69    /// composition audit. Whatever it finds, **the frames are served either
70    /// way** (`SPEC.md` F9).
71    async fn query(&self, query: &ContextQuery) -> Result<ContextQueryResult, HostError>;
72
73    /// Revalidate frames the host already holds, without any frame body
74    /// travelling (`docs/context-reuse.md` §4 `context/verify`).
75    ///
76    /// Defaults to answering [`Verdict::Unknown`](contextgraph_types::Verdict::Unknown)
77    /// for every requested identity, so an existing provider implements
78    /// nothing and is simply treated as unable to vouch for its frames — the
79    /// host then re-queries them. A provider that overrides this **MUST** also
80    /// advertise [`Capabilities::verify`](contextgraph_types::Capabilities::verify),
81    /// since the host only asks providers that declare support.
82    async fn verify(&self, request: &VerifyRequest) -> Result<VerifyResponse, HostError> {
83        Ok(VerifyResponse::uniform(request, Verdict::Unknown))
84    }
85
86    /// Shut the provider down cleanly (SPEC.md §3 lifecycle). In-process providers
87    /// default to a no-op; transport-backed providers send `shutdown` and
88    /// reap their child. Overridable.
89    async fn shutdown(&self) -> Result<(), HostError> {
90        Ok(())
91    }
92}
93
94/// The snake_case wire name of a [`FrameKind`], matching its `serde`
95/// representation and the strings a provider lists in
96/// [`Capabilities::query`]'s `kinds`.
97///
98/// A thin delegation to [`FrameKind::as_str`], kept because several call sites
99/// read better as a function. It used to be a hand-written `match` returning
100/// `&'static str`; that duplicated the vocabulary in a second place and, worse,
101/// could not name a kind the host did not know. Now that the vocabulary is open
102/// the borrow is tied to the kind, because an unknown kind owns its string.
103pub fn frame_kind_name(kind: &FrameKind) -> &str {
104    kind.as_str()
105}
106
107/// Whether a provider is worth querying for a given request. A query with no
108/// `kinds` filter matches every provider (the host wants everyone's best
109/// frames, SPEC.md §5); otherwise a provider matches when it declares at least one
110/// of the requested frame kinds. Used by `query_all` to fan out only to
111/// relevant providers.
112pub fn capability_matches(caps: &Capabilities, query: &ContextQuery) -> bool {
113    if query.kinds.is_empty() {
114        return true;
115    }
116    query.kinds.iter().any(|requested| {
117        let name = frame_kind_name(requested);
118        caps.query.kinds.iter().any(|served| served == name)
119    })
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use contextgraph_types::capability::QueryCapability;
126
127    fn caps_for(kinds: &[&str]) -> Capabilities {
128        Capabilities {
129            query: QueryCapability {
130                kinds: kinds.iter().map(|k| k.to_string()).collect(),
131            },
132            ..Capabilities::default()
133        }
134    }
135
136    fn query_for(kinds: Vec<FrameKind>) -> ContextQuery {
137        ContextQuery {
138            goal: "g".into(),
139            query_text: None,
140            embedding: None,
141            kinds,
142            anchors: vec![],
143            max_frames: 5,
144            max_tokens: 1000,
145            as_of: None,
146            representation_preferences: vec![],
147        }
148    }
149
150    #[test]
151    fn frame_kind_names_match_serde_snake_case() {
152        // The names a provider lists must be exactly the frames' serde names.
153        for (kind, name) in [
154            (FrameKind::Snippet, "snippet"),
155            (FrameKind::Symbol, "symbol"),
156            (FrameKind::Fact, "fact"),
157            (FrameKind::Doc, "doc"),
158            (FrameKind::Memory, "memory"),
159            (FrameKind::Episode, "episode"),
160            (FrameKind::Graph, "graph"),
161        ] {
162            assert_eq!(frame_kind_name(&kind), name);
163            let serde_name = serde_json::to_value(&kind).unwrap();
164            assert_eq!(serde_name, serde_json::Value::String(name.to_string()));
165        }
166    }
167
168    #[test]
169    fn an_empty_kind_filter_matches_every_provider() {
170        let caps = caps_for(&["doc"]);
171        assert!(capability_matches(&caps, &query_for(vec![])));
172    }
173
174    #[test]
175    fn a_kind_filter_matches_only_overlapping_providers() {
176        let doc_provider = caps_for(&["doc", "snippet"]);
177        assert!(capability_matches(
178            &doc_provider,
179            &query_for(vec![FrameKind::Doc])
180        ));
181        assert!(capability_matches(
182            &doc_provider,
183            &query_for(vec![FrameKind::Fact, FrameKind::Snippet])
184        ));
185        assert!(!capability_matches(
186            &doc_provider,
187            &query_for(vec![FrameKind::Memory, FrameKind::Episode])
188        ));
189    }
190}