Skip to main content

imp_core/
trust.rs

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}