1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(default)]
7pub struct Provenance {
8 pub id: Option<String>,
9 pub source: ProvenanceSource,
10 pub trust: TrustLabel,
11 pub risk: Vec<RiskLabel>,
12 pub origin: Option<String>,
13 pub artifact_ref: Option<PathBuf>,
14 pub derived_from: Vec<DerivedFrom>,
15 pub notes: Vec<String>,
16}
17
18impl Provenance {
19 pub fn new(source: ProvenanceSource) -> Self {
20 let trust = source.default_trust_label();
21 let risk = source.default_risk_labels();
22 Self {
23 source,
24 trust,
25 risk,
26 ..Self::default()
27 }
28 }
29
30 pub fn user_instruction() -> Self {
31 Self::new(ProvenanceSource::UserInstruction)
32 }
33
34 pub fn workspace_file(path: impl Into<PathBuf>) -> Self {
35 let path = path.into();
36 let mut provenance = Self::new(ProvenanceSource::WorkspaceFile { path: path.clone() });
37 provenance.origin = Some(path.display().to_string());
38 provenance
39 }
40
41 pub fn external_web(url: impl Into<String>) -> Self {
42 let url = url.into();
43 let mut provenance = Self::new(ProvenanceSource::ExternalWebContent {
44 url: Some(url.clone()),
45 });
46 provenance.origin = Some(url);
47 provenance
48 }
49
50 pub fn tool_observation(tool_name: impl Into<String>) -> Self {
51 Self::new(ProvenanceSource::ToolObservation {
52 tool_name: Some(tool_name.into()),
53 })
54 }
55
56 pub fn verifier_output(gate_id: impl Into<String>) -> Self {
57 Self::new(ProvenanceSource::VerifierOutput {
58 gate_id: Some(gate_id.into()),
59 })
60 }
61
62 pub fn durable_memory(key: impl Into<String>) -> Self {
63 Self::new(ProvenanceSource::DurableMemory {
64 key: Some(key.into()),
65 })
66 }
67
68 pub fn generated_summary(parents: impl IntoIterator<Item = Provenance>) -> Self {
69 let parents: Vec<Provenance> = parents.into_iter().collect();
70 let mut provenance = Self::new(ProvenanceSource::GeneratedSummary);
71 provenance.derived_from = parents.iter().map(DerivedFrom::from).collect();
72 provenance.trust = lowest_authority_trust(parents.iter().map(|parent| parent.trust));
73 provenance.risk = merge_risk_labels(
74 parents
75 .iter()
76 .flat_map(|parent| parent.risk.iter().copied())
77 .chain([RiskLabel::Generated]),
78 );
79 provenance
80 }
81
82 pub fn mana_record(kind: ManaRecordKind, unit_id: impl Into<String>) -> Self {
83 let unit_id = unit_id.into();
84 let mut provenance = Self::new(ProvenanceSource::ManaRecord {
85 kind,
86 unit_id: Some(unit_id.clone()),
87 });
88 provenance.origin = Some(unit_id);
89 provenance
90 }
91
92 pub fn with_risk(mut self, risk: RiskLabel) -> Self {
93 if !self.risk.contains(&risk) {
94 self.risk.push(risk);
95 }
96 self
97 }
98
99 pub fn with_note(mut self, note: impl Into<String>) -> Self {
100 self.notes.push(note.into());
101 self
102 }
103
104 pub fn is_low_trust(&self) -> bool {
105 self.trust.is_low_trust() || self.risk.contains(&RiskLabel::LowTrust)
106 }
107}
108
109impl Default for Provenance {
110 fn default() -> Self {
111 Self {
112 id: None,
113 source: ProvenanceSource::Unknown,
114 trust: TrustLabel::Unknown,
115 risk: vec![RiskLabel::LowTrust],
116 origin: None,
117 artifact_ref: None,
118 derived_from: Vec::new(),
119 notes: Vec::new(),
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(default)]
126pub struct TrustedContext<T> {
127 pub value: T,
128 pub provenance: Provenance,
129}
130
131impl<T> TrustedContext<T> {
132 pub fn new(value: T, provenance: Provenance) -> Self {
133 Self { value, provenance }
134 }
135
136 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> TrustedContext<U> {
137 TrustedContext {
138 value: f(self.value),
139 provenance: self.provenance,
140 }
141 }
142}
143
144impl<T: Default> Default for TrustedContext<T> {
145 fn default() -> Self {
146 Self {
147 value: T::default(),
148 provenance: Provenance::default(),
149 }
150 }
151}
152
153#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case")]
155pub enum TrustLabel {
156 UserInstruction,
157 ProjectTrusted,
158 ToolObserved,
159 ExternalUntrusted,
160 DurableMemory,
161 GeneratedSummary,
162 VerifierOutput,
163 ManaLedger,
164 #[default]
165 Unknown,
166}
167
168impl TrustLabel {
169 pub fn is_low_trust(self) -> bool {
170 matches!(
171 self,
172 Self::ExternalUntrusted | Self::GeneratedSummary | Self::Unknown
173 )
174 }
175
176 fn authority_rank(self) -> u8 {
177 match self {
178 Self::Unknown => 0,
179 Self::ExternalUntrusted => 1,
180 Self::GeneratedSummary => 2,
181 Self::ToolObserved => 3,
182 Self::DurableMemory => 4,
183 Self::ProjectTrusted => 5,
184 Self::VerifierOutput => 6,
185 Self::ManaLedger => 7,
186 Self::UserInstruction => 8,
187 }
188 }
189}
190
191#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "kebab-case", tag = "source")]
193pub enum ProvenanceSource {
194 UserInstruction,
195 WorkspaceFile {
196 path: PathBuf,
197 },
198 ExternalWebContent {
199 url: Option<String>,
200 },
201 ToolObservation {
202 tool_name: Option<String>,
203 },
204 VerifierOutput {
205 gate_id: Option<String>,
206 },
207 DurableMemory {
208 key: Option<String>,
209 },
210 GeneratedSummary,
211 ManaRecord {
212 kind: ManaRecordKind,
213 unit_id: Option<String>,
214 },
215 SystemPolicy,
216 Extension {
217 id: String,
218 },
219 #[default]
220 Unknown,
221}
222
223impl ProvenanceSource {
224 pub fn default_trust_label(&self) -> TrustLabel {
225 match self {
226 Self::UserInstruction => TrustLabel::UserInstruction,
227 Self::WorkspaceFile { .. } | Self::SystemPolicy => TrustLabel::ProjectTrusted,
228 Self::ExternalWebContent { .. } | Self::Unknown => TrustLabel::ExternalUntrusted,
229 Self::ToolObservation { .. } => TrustLabel::ToolObserved,
230 Self::VerifierOutput { .. } => TrustLabel::VerifierOutput,
231 Self::DurableMemory { .. } => TrustLabel::DurableMemory,
232 Self::GeneratedSummary => TrustLabel::GeneratedSummary,
233 Self::ManaRecord { .. } => TrustLabel::ManaLedger,
234 Self::Extension { .. } => TrustLabel::ToolObserved,
235 }
236 }
237
238 pub fn default_risk_labels(&self) -> Vec<RiskLabel> {
239 match self {
240 Self::UserInstruction => vec![RiskLabel::UserAuthoritative],
241 Self::WorkspaceFile { .. } | Self::SystemPolicy => vec![RiskLabel::ProjectPolicy],
242 Self::ExternalWebContent { .. } => vec![
243 RiskLabel::External,
244 RiskLabel::LowTrust,
245 RiskLabel::NetworkDerived,
246 ],
247 Self::ToolObservation { .. } => vec![RiskLabel::ToolOutput],
248 Self::VerifierOutput { .. } => vec![RiskLabel::VerificationArtifact],
249 Self::DurableMemory { .. } => vec![RiskLabel::DurableLedger],
250 Self::GeneratedSummary => vec![RiskLabel::Generated],
251 Self::ManaRecord { .. } => vec![RiskLabel::DurableLedger],
252 Self::Extension { .. } => vec![RiskLabel::ToolOutput],
253 Self::Unknown => vec![RiskLabel::LowTrust],
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
259#[serde(rename_all = "kebab-case")]
260pub enum RiskLabel {
261 LowTrust,
262 UserAuthoritative,
263 ProjectPolicy,
264 External,
265 Stale,
266 Generated,
267 ToolOutput,
268 ContainsInstructions,
269 PossiblePromptInjection,
270 SecretAdjacent,
271 NetworkDerived,
272 VerificationArtifact,
273 DurableLedger,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(default)]
278pub struct DerivedFrom {
279 pub provenance_id: Option<String>,
280 pub source: ProvenanceSource,
281 pub trust: TrustLabel,
282 pub risk: Vec<RiskLabel>,
283 pub origin: Option<String>,
284}
285
286impl From<&Provenance> for DerivedFrom {
287 fn from(provenance: &Provenance) -> Self {
288 Self {
289 provenance_id: provenance.id.clone(),
290 source: provenance.source.clone(),
291 trust: provenance.trust,
292 risk: provenance.risk.clone(),
293 origin: provenance.origin.clone(),
294 }
295 }
296}
297
298impl Default for DerivedFrom {
299 fn default() -> Self {
300 Self {
301 provenance_id: None,
302 source: ProvenanceSource::Unknown,
303 trust: TrustLabel::Unknown,
304 risk: vec![RiskLabel::LowTrust],
305 origin: None,
306 }
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
311#[serde(rename_all = "kebab-case")]
312pub enum ManaRecordKind {
313 Fact,
314 Note,
315 Decision,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
319#[serde(rename_all = "kebab-case")]
320pub enum TrustBoundary {
321 User,
322 Project,
323 External,
324 Tool,
325 Verifier,
326 Memory,
327 ManaLedger,
328 Generated,
329 Extension,
330 Unknown,
331}
332
333impl From<&ProvenanceSource> for TrustBoundary {
334 fn from(source: &ProvenanceSource) -> Self {
335 match source {
336 ProvenanceSource::UserInstruction => Self::User,
337 ProvenanceSource::WorkspaceFile { .. } | ProvenanceSource::SystemPolicy => {
338 Self::Project
339 }
340 ProvenanceSource::ExternalWebContent { .. } => Self::External,
341 ProvenanceSource::ToolObservation { .. } => Self::Tool,
342 ProvenanceSource::VerifierOutput { .. } => Self::Verifier,
343 ProvenanceSource::DurableMemory { .. } => Self::Memory,
344 ProvenanceSource::GeneratedSummary => Self::Generated,
345 ProvenanceSource::ManaRecord { .. } => Self::ManaLedger,
346 ProvenanceSource::Extension { .. } => Self::Extension,
347 ProvenanceSource::Unknown => Self::Unknown,
348 }
349 }
350}
351
352fn lowest_authority_trust(trusts: impl IntoIterator<Item = TrustLabel>) -> TrustLabel {
353 trusts
354 .into_iter()
355 .min_by_key(|trust| trust.authority_rank())
356 .unwrap_or(TrustLabel::GeneratedSummary)
357}
358
359fn merge_risk_labels(labels: impl IntoIterator<Item = RiskLabel>) -> Vec<RiskLabel> {
360 let mut merged = Vec::new();
361 for label in labels {
362 if !merged.contains(&label) {
363 merged.push(label);
364 }
365 }
366 merged
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn trust_labels_default_source_categories() {
375 assert_eq!(
376 Provenance::user_instruction().trust,
377 TrustLabel::UserInstruction
378 );
379 assert_eq!(
380 Provenance::workspace_file("src/lib.rs").trust,
381 TrustLabel::ProjectTrusted
382 );
383 let web = Provenance::external_web("https://example.com");
384 assert_eq!(web.trust, TrustLabel::ExternalUntrusted);
385 assert!(web.risk.contains(&RiskLabel::LowTrust));
386 assert!(web.risk.contains(&RiskLabel::NetworkDerived));
387 assert_eq!(
388 Provenance::tool_observation("bash").trust,
389 TrustLabel::ToolObserved
390 );
391 assert_eq!(
392 Provenance::verifier_output("unit").trust,
393 TrustLabel::VerifierOutput
394 );
395 assert_eq!(
396 Provenance::durable_memory("project-style").trust,
397 TrustLabel::DurableMemory
398 );
399 assert_eq!(
400 Provenance::mana_record(ManaRecordKind::Fact, "394.8").trust,
401 TrustLabel::ManaLedger
402 );
403 }
404
405 #[test]
406 fn trust_labels_serde_roundtrip() {
407 let provenance = Provenance::external_web("https://example.com")
408 .with_risk(RiskLabel::PossiblePromptInjection)
409 .with_note("observed instruction-like content");
410 let json = serde_json::to_string(&provenance).unwrap();
411 assert!(json.contains("external-web-content"));
412 assert!(json.contains("possible-prompt-injection"));
413 let decoded: Provenance = serde_json::from_str(&json).unwrap();
414 assert_eq!(decoded, provenance);
415 }
416
417 #[test]
418 fn trust_labels_generated_summary_preserves_derivation_and_lowest_trust() {
419 let mut user = Provenance::user_instruction();
420 user.id = Some("ctx_user".into());
421 let mut web = Provenance::external_web("https://example.com")
422 .with_risk(RiskLabel::ContainsInstructions)
423 .with_risk(RiskLabel::PossiblePromptInjection);
424 web.id = Some("ctx_web".into());
425
426 let summary = Provenance::generated_summary([user.clone(), web.clone()]);
427 assert_eq!(summary.trust, TrustLabel::ExternalUntrusted);
428 assert!(summary.risk.contains(&RiskLabel::Generated));
429 assert!(summary.risk.contains(&RiskLabel::PossiblePromptInjection));
430 assert_eq!(summary.derived_from.len(), 2);
431 assert_eq!(
432 summary.derived_from[0].provenance_id.as_deref(),
433 Some("ctx_user")
434 );
435 assert_eq!(
436 summary.derived_from[1].provenance_id.as_deref(),
437 Some("ctx_web")
438 );
439 }
440
441 #[test]
442 fn trust_labels_trusted_context_maps_value_without_losing_provenance() {
443 let context =
444 TrustedContext::new("hello".to_string(), Provenance::workspace_file("README.md"));
445 let mapped = context.map(|value| value.len());
446 assert_eq!(mapped.value, 5);
447 assert_eq!(mapped.provenance.trust, TrustLabel::ProjectTrusted);
448 assert_eq!(
449 TrustBoundary::from(&mapped.provenance.source),
450 TrustBoundary::Project
451 );
452 }
453}