1use serde::{Deserialize, Serialize};
5
6use super::provider_trait::ProviderParams;
7use super::{ProviderItem, ProviderResult};
8
9#[cfg(test)]
10#[path = "provenance_tests.rs"]
11mod tests;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
22pub(crate) struct ReuseRefusal {
23 pub(crate) validity: lean_ctx_protocol::context_lineage::SourceValidityV1,
24 pub(crate) reason: String,
25}
26
27impl ReuseRefusal {
28 fn new(
29 validity: lean_ctx_protocol::context_lineage::SourceValidityV1,
30 reason: impl Into<String>,
31 ) -> Self {
32 Self {
33 validity,
34 reason: reason.into(),
35 }
36 }
37
38 fn of_reacquisition(error: String) -> Self {
43 use lean_ctx_protocol::context_lineage::SourceValidityV1 as V;
44 let lower = error.to_ascii_lowercase();
45 let access_status = lower
46 .split(|c: char| !c.is_ascii_digit())
47 .any(|number| matches!(number, "401" | "403" | "404" | "410"));
48 let validity = if lower.contains("does not support current item authorization") {
49 V::Unsupported
50 } else if access_status || lower.contains("forbidden") || lower.contains("not found") {
51 V::Revoked
52 } else {
53 V::Unavailable
54 };
55 Self::new(validity, error)
56 }
57}
58
59impl std::fmt::Display for ReuseRefusal {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.write_str(&self.reason)
62 }
63}
64
65impl From<ReuseRefusal> for String {
66 fn from(refusal: ReuseRefusal) -> Self {
67 refusal.reason
68 }
69}
70
71tokio::task_local! {
72 static REUSE_DEADLINE: std::time::Instant;
73 static REUSE_CHECKS: std::cell::Cell<usize>;
74}
75
76pub(crate) fn with_reuse_deadline<T>(operation: impl FnOnce() -> T) -> T {
77 with_reuse_deadline_until(None, operation)
78}
79
80pub(crate) fn with_reuse_deadline_until<T>(
86 outer: Option<std::time::Instant>,
87 operation: impl FnOnce() -> T,
88) -> T {
89 if let Ok(enclosing) = REUSE_DEADLINE.try_with(|end| *end) {
90 return match outer {
91 Some(outer) if outer < enclosing => REUSE_DEADLINE.sync_scope(outer, operation),
92 _ => operation(),
93 };
94 }
95 let own = std::time::Instant::now() + std::time::Duration::from_secs(5);
96 let end = outer.map_or(own, |outer| outer.min(own));
97 REUSE_CHECKS.sync_scope(std::cell::Cell::new(32), || {
98 REUSE_DEADLINE.sync_scope(end, operation)
99 })
100}
101
102fn reserve_reuse_check() -> Result<(), String> {
103 REUSE_CHECKS
104 .try_with(|remaining| {
105 let next = remaining
106 .get()
107 .checked_sub(1)
108 .ok_or("provider reuse check budget exhausted")?;
109 remaining.set(next);
110 Ok(())
111 })
112 .map_err(|_| "provider reuse check scope is unavailable")?
113}
114
115pub(crate) fn reuse_time_remaining() -> Option<std::time::Duration> {
117 REUSE_DEADLINE
118 .try_with(|end| end.saturating_duration_since(std::time::Instant::now()))
119 .ok()
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(deny_unknown_fields)]
124pub struct ProviderOrigin {
125 pub version: u16,
126 pub provider: String,
127 pub resource: String,
128 pub binding: String,
129 pub params: ProviderParams,
130 pub item_id: String,
131 pub item_digest: String,
132 #[serde(default)]
133 pub projection: Projection,
134}
135
136#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
137#[serde(rename_all = "snake_case")]
138pub enum Projection {
139 #[default]
140 Raw,
141 SnapshotV1,
142}
143
144impl Projection {
145 fn item(self, item: &ProviderItem) -> Result<ProviderItem, String> {
146 if self == Self::Raw {
147 return Ok(item.clone());
148 }
149 let mut value = serde_json::to_value(item).map_err(|_| "provider item is invalid")?;
150 super::snapshot_projection::redact_and_bound(&mut value, None);
151 serde_json::from_value(value).map_err(|_| "provider projection is invalid".into())
152 }
153}
154
155pub struct BoundResult {
158 pub result: ProviderResult,
159 provider: String,
160 resource: String,
161 params: ProviderParams,
162 binding: Option<String>,
163}
164
165pub(crate) struct CurrentProviderChunks {
166 pub(crate) chunks: Vec<crate::core::content_chunk::ContentChunk>,
167 pub(crate) original_bytes: usize,
169}
170
171pub(crate) fn digest(value: &impl Serialize) -> Option<String> {
172 let json = serde_json::to_value(value).ok()?;
173 Some(
174 blake3::hash(&crate::core::canonical::canonical_serialize(&json))
175 .to_hex()
176 .to_string(),
177 )
178}
179
180impl BoundResult {
181 pub fn execute(
182 provider: &dyn super::provider_trait::ContextProvider,
183 action: &str,
184 params: &ProviderParams,
185 ) -> Result<Self, String> {
186 let before = provider.reuse_binding(action, params);
187 let result = provider.execute(action, params)?;
188 if before.is_some() && (result.provider != provider.id() || result.resource_type != action)
189 {
190 return Err("provider result does not match its requested source".into());
191 }
192 if before != provider.reuse_binding(action, params) {
193 return Err("provider source changed during acquisition".into());
194 }
195 Ok(Self {
196 result,
197 provider: provider.id().into(),
198 resource: action.into(),
199 params: params.clone(),
200 binding: before,
201 })
202 }
203
204 pub fn chunks(
205 &self,
206 projection: &ProviderResult,
207 ) -> Vec<crate::core::content_chunk::ContentChunk> {
208 self.chunks_with_projection(projection, Projection::Raw)
209 }
210
211 pub fn chunks_with_projection(
212 &self,
213 projection: &ProviderResult,
214 kind: Projection,
215 ) -> Vec<crate::core::content_chunk::ContentChunk> {
216 let mut chunks = super::registry::result_to_chunks(projection);
217 for (chunk, item) in chunks.iter_mut().zip(&projection.items) {
218 let Some(binding) = &self.binding else {
219 continue;
220 };
221 let mut originals = self
222 .result
223 .items
224 .iter()
225 .filter(|original| original.id == item.id);
226 let Some(original) = originals.next() else {
227 continue;
228 };
229 if originals.next().is_some() {
230 continue;
231 }
232 let scope = &blake3::hash(binding.as_bytes()).to_hex()[..12];
238 chunk.file_path = format!("{}://{scope}/{}/{}", self.provider, self.resource, item.id);
239 chunk.origin = digest(original).map(|item_digest| ProviderOrigin {
240 version: 1,
241 provider: self.provider.clone(),
242 resource: self.resource.clone(),
243 binding: binding.clone(),
244 params: self.params.clone(),
245 item_id: item.id.clone(),
246 item_digest,
247 projection: kind,
248 });
249 }
250 chunks
251 }
252}
253
254impl ProviderOrigin {
255 pub(crate) fn current_facts(
258 &self,
259 project_root: &str,
260 ) -> Result<Vec<crate::core::knowledge_provider_extract::ExtractedFact>, String> {
261 let view = self.current_chunks(project_root)?;
262 Ok(crate::core::knowledge_provider_extract::extract_facts(
263 &view.chunks,
264 ))
265 }
266
267 pub(crate) fn current_chunks(
270 &self,
271 project_root: &str,
272 ) -> Result<CurrentProviderChunks, ReuseRefusal> {
273 use lean_ctx_protocol::context_lineage::SourceValidityV1 as V;
274 let refuse = |validity: V, reason: &str| ReuseRefusal::new(validity, reason);
275 if reuse_time_remaining().is_none_or(|remaining| remaining.is_zero()) {
276 return Err(refuse(
277 V::Unavailable,
278 "provider reuse deadline is unavailable or exhausted",
279 ));
280 }
281 reserve_reuse_check().map_err(|reason| ReuseRefusal::new(V::Unavailable, reason))?;
282 if self.version != 1
283 || self.binding.len() != 64
284 || self.item_digest.len() != 64
285 || self.provider.len() > 256
286 || self.resource.len() > 256
287 || self.item_id.len() > 4096
288 || serde_json::to_vec(&self.params).map_or(true, |v| v.len() > 16 * 1024)
289 {
290 return Err(refuse(V::Unsupported, "stored provider origin is invalid"));
291 }
292 if !crate::core::roles::active_role().is_tool_allowed("ctx_provider")
293 || crate::core::policy::runtime::active()
294 .is_some_and(|p| !p.tool_allowed("ctx_provider"))
295 {
296 return Err(refuse(
297 V::RevalidateRequired,
298 "provider access is not authorized",
299 ));
300 }
301 let root = std::path::Path::new(project_root);
302 let request = crate::core::policy::diagnostics::request_project().ok_or_else(|| {
303 refuse(
304 V::RevalidateRequired,
305 "provider reuse requires a project scope",
306 )
307 })?;
308 if crate::core::pathutil::safe_canonicalize_bounded(&request, 2000)
309 != crate::core::pathutil::safe_canonicalize_bounded(root, 2000)
310 {
311 return Err(refuse(
312 V::Revoked,
313 "provider reuse belongs to another project",
314 ));
315 }
316 if !crate::core::config::Config::load().providers.enabled {
317 return Err(refuse(V::RevalidateRequired, "providers are disabled"));
318 }
319 let provider: std::sync::Arc<dyn super::provider_trait::ContextProvider> =
323 if let Some(selected) = super::registry::global_registry().get_pinned(&self.provider) {
324 selected
325 } else if let Some(builtin) = super::registry::builtin_for_reuse(&self.provider) {
326 builtin
327 } else {
328 let config = super::config_provider::discovery::discover_configs(Some(root))
329 .into_iter()
330 .find(|entry| entry.config.id == self.provider)
331 .ok_or_else(|| refuse(V::Unavailable, "stored provider is unavailable"))?;
332 std::sync::Arc::new(
333 super::config_provider::ConfigProvider::from_config(config.config)
334 .map_err(|reason| ReuseRefusal::new(V::Unavailable, reason))?,
335 )
336 };
337 if !provider.is_available()
338 || !provider
339 .supported_actions()
340 .contains(&self.resource.as_str())
341 || provider
342 .reuse_binding(&self.resource, &self.params)
343 .as_ref()
344 != Some(&self.binding)
345 {
346 return Err(refuse(
347 V::Revoked,
348 "stored provider binding is no longer authorized",
349 ));
350 }
351 let result = provider
352 .reacquire_item(&self.resource, &self.params, &self.item_id)
353 .map_err(ReuseRefusal::of_reacquisition)?;
354 if provider
355 .reuse_binding(&self.resource, &self.params)
356 .as_ref()
357 != Some(&self.binding)
358 || result.provider != self.provider
359 || result.resource_type != self.resource
360 {
361 return Err(refuse(
362 V::RevalidateRequired,
363 "provider source changed during reauthorization",
364 ));
365 }
366 let mut items = result.items.iter().filter(|item| item.id == self.item_id);
367 let item: &ProviderItem = items
368 .next()
369 .ok_or_else(|| refuse(V::Deleted, "stored resource is no longer accessible"))?;
370 if items.next().is_some() || digest(item).as_ref() != Some(&self.item_digest) {
371 return Err(refuse(
372 V::Stale,
373 "stored resource revision is no longer current",
374 ));
375 }
376 let projected = ProviderResult {
377 provider: self.provider.clone(),
378 resource_type: self.resource.clone(),
379 items: vec![
380 self.projection
381 .item(item)
382 .map_err(|reason| ReuseRefusal::new(V::Unsupported, reason))?,
383 ],
384 total_count: Some(1),
385 truncated: false,
386 };
387 let mut chunks = super::registry::result_to_chunks(&projected);
388 let rendered_bytes = chunks
389 .iter()
390 .try_fold(0usize, |sum, chunk| sum.checked_add(chunk.content.len()))
391 .ok_or_else(|| refuse(V::Unsupported, "provider source size overflow"))?;
392 let original_bytes = crate::core::canonical::canonical_serialize(item)
393 .len()
394 .max(rendered_bytes);
395 for chunk in &mut chunks {
396 chunk.content = crate::core::policy::content::protect_active(&chunk.content)
397 .map_err(|reason| ReuseRefusal::new(V::RevalidateRequired, reason))?
398 .into_owned();
399 }
400 Ok(CurrentProviderChunks {
401 chunks,
402 original_bytes,
403 })
404 }
405}