Skip to main content

mnemo_core/query/
write_provenance.rs

1//! Engine surface for write provenance.
2//!
3//! - **Record** a tamper-evident provenance entry on the REMEMBER / SHARE write
4//!   path (chained onto the store's head).
5//! - **Query** it: by memory id, by principal, by session/trace id.
6//! - **Verify** the whole chain is intact (tamper-evidence).
7//! - **FORGET BY PROVENANCE**: revoke everything a principal or session wrote in
8//!   one call. This is the point — after a poisoning incident, targeted cleanup
9//!   by the responsible principal/session instead of wiping the store.
10
11use uuid::Uuid;
12
13use crate::error::{Error, Result};
14use crate::hash::ChainVerificationResult;
15use crate::model::capability::Capability;
16use crate::model::write_provenance::{
17    WriteFlag, WriteOp, WriteProvenance, verify_provenance_chain,
18};
19use crate::query::MnemoEngine;
20use crate::query::forget::{self, ForgetRequest, ForgetResponse, ForgetStrategy};
21
22impl MnemoEngine {
23    /// Record a tamper-evident write-provenance record for `memory_id`, chained
24    /// onto the store's current head. No-op if the backend does not record
25    /// provenance. Called from the REMEMBER / SHARE write paths.
26    pub(crate) async fn record_write_provenance(
27        &self,
28        memory_id: Uuid,
29        principal: String,
30        capability_id: Option<Uuid>,
31        session_id: Option<String>,
32        op: WriteOp,
33        flags: Vec<WriteFlag>,
34    ) -> Result<()> {
35        if !self.storage.records_write_provenance() {
36            return Ok(());
37        }
38        let prev_hash = self.storage.get_latest_provenance_hash().await?;
39        let prov = WriteProvenance::new(
40            memory_id,
41            principal,
42            capability_id,
43            session_id,
44            op,
45            flags,
46            prev_hash,
47        );
48        self.storage.insert_write_provenance(&prov).await
49    }
50
51    /// Verify a presented [`Capability`] against the configured issuer. Errors if
52    /// no issuer is attached or the capability is invalid / expired.
53    pub fn verify_capability(&self, cap: &Capability) -> Result<()> {
54        let issuer = self.capability_issuer.as_ref().ok_or_else(|| {
55            Error::PermissionDenied(
56                "a capability was presented but no CapabilityIssuer is configured".to_string(),
57            )
58        })?;
59        issuer
60            .verify(cap)
61            .map_err(|e| Error::PermissionDenied(format!("capability rejected: {e}")))
62    }
63
64    /// Provenance for one memory (its most recent write), if recorded.
65    pub async fn write_provenance_for(&self, memory_id: Uuid) -> Result<Option<WriteProvenance>> {
66        self.storage.get_write_provenance(memory_id).await
67    }
68
69    /// Everything `principal` wrote, newest first (up to `limit`).
70    pub async fn writes_by_principal(
71        &self,
72        principal: &str,
73        limit: usize,
74    ) -> Result<Vec<WriteProvenance>> {
75        self.storage
76            .list_provenance_by_principal(principal, limit)
77            .await
78    }
79
80    /// Everything written under `session_id`, newest first (up to `limit`).
81    pub async fn writes_by_session(
82        &self,
83        session_id: &str,
84        limit: usize,
85    ) -> Result<Vec<WriteProvenance>> {
86        self.storage
87            .list_provenance_by_session(session_id, limit)
88            .await
89    }
90
91    /// Verify the whole write-provenance chain is intact — tamper-evidence over
92    /// the append history. `limit` bounds how far back to walk.
93    pub async fn verify_provenance_chain(&self, limit: usize) -> Result<ChainVerificationResult> {
94        let recs = self.storage.list_all_provenance(limit).await?;
95        Ok(verify_provenance_chain(&recs))
96    }
97
98    /// FORGET BY PROVENANCE: revoke everything `principal` authored (REMEMBER),
99    /// using `strategy` (SoftDelete / HardDelete / Redact), in one call. This is
100    /// remediation — targeted at the responsible principal — not a wipe.
101    pub async fn forget_by_principal(
102        &self,
103        principal: &str,
104        strategy: ForgetStrategy,
105    ) -> Result<ForgetResponse> {
106        let ids = self.storage.list_memory_ids_by_principal(principal).await?;
107        self.forget_ids(ids, strategy).await
108    }
109
110    /// FORGET BY PROVENANCE by session / trace id.
111    pub async fn forget_by_session(
112        &self,
113        session_id: &str,
114        strategy: ForgetStrategy,
115    ) -> Result<ForgetResponse> {
116        let ids = self.storage.list_memory_ids_by_session(session_id).await?;
117        self.forget_ids(ids, strategy).await
118    }
119
120    async fn forget_ids(&self, ids: Vec<Uuid>, strategy: ForgetStrategy) -> Result<ForgetResponse> {
121        if ids.is_empty() {
122            return Ok(ForgetResponse {
123                forgotten: Vec::new(),
124                errors: Vec::new(),
125            });
126        }
127        let request = ForgetRequest {
128            memory_ids: ids,
129            agent_id: None,
130            strategy: Some(strategy),
131            criteria: None,
132        };
133        forget::execute(self, request).await
134    }
135}