use std::sync::Mutex;
use chio_core::canonical::canonical_json_bytes;
use chio_core::crypto::sha256_hex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub const MEMORY_PROVENANCE_ENTRY_SCHEMA: &str = "chio.memory_provenance_entry.v1";
pub const MEMORY_PROVENANCE_GENESIS_PREV_HASH: &str =
"0000000000000000000000000000000000000000000000000000000000000000";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryProvenanceEntry {
pub entry_id: String,
pub store: String,
pub key: String,
pub capability_id: String,
pub receipt_id: String,
pub written_at: u64,
pub prev_hash: String,
pub hash: String,
}
#[derive(Debug, Clone, Serialize)]
struct MemoryProvenanceHashInput<'a> {
schema: &'a str,
entry_id: &'a str,
store: &'a str,
key: &'a str,
capability_id: &'a str,
receipt_id: &'a str,
written_at: u64,
prev_hash: &'a str,
}
impl MemoryProvenanceEntry {
pub fn expected_hash(&self) -> Result<String, MemoryProvenanceError> {
recompute_entry_hash(
&self.entry_id,
&self.store,
&self.key,
&self.capability_id,
&self.receipt_id,
self.written_at,
&self.prev_hash,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryProvenanceAppend {
pub store: String,
pub key: String,
pub capability_id: String,
pub receipt_id: String,
pub written_at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum ProvenanceVerification {
Verified {
entry: MemoryProvenanceEntry,
chain_digest: String,
},
Unverified { reason: UnverifiedReason },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UnverifiedReason {
NoProvenance,
ChainTampered,
ChainLinkBroken,
StoreUnavailable,
}
impl UnverifiedReason {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::NoProvenance => "no_provenance",
Self::ChainTampered => "chain_tampered",
Self::ChainLinkBroken => "chain_link_broken",
Self::StoreUnavailable => "store_unavailable",
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MemoryProvenanceError {
#[error("memory provenance store backend error: {0}")]
Backend(String),
#[error("memory provenance canonical serialization failed: {0}")]
Serialization(String),
#[error("memory provenance entry not found: {0}")]
NotFound(String),
}
pub trait MemoryProvenanceStore: Send + Sync {
fn append(
&self,
input: MemoryProvenanceAppend,
) -> Result<MemoryProvenanceEntry, MemoryProvenanceError>;
fn get_entry(
&self,
entry_id: &str,
) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError>;
fn latest_for_key(
&self,
store: &str,
key: &str,
) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError>;
fn verify_entry(&self, entry_id: &str)
-> Result<ProvenanceVerification, MemoryProvenanceError>;
fn chain_digest(&self) -> Result<String, MemoryProvenanceError>;
}
pub fn recompute_entry_hash(
entry_id: &str,
store: &str,
key: &str,
capability_id: &str,
receipt_id: &str,
written_at: u64,
prev_hash: &str,
) -> Result<String, MemoryProvenanceError> {
let input = MemoryProvenanceHashInput {
schema: MEMORY_PROVENANCE_ENTRY_SCHEMA,
entry_id,
store,
key,
capability_id,
receipt_id,
written_at,
prev_hash,
};
let bytes = canonical_json_bytes(&input)
.map_err(|error| MemoryProvenanceError::Serialization(error.to_string()))?;
Ok(sha256_hex(&bytes))
}
#[must_use]
pub fn next_entry_id() -> String {
format!("mem-prov-{}", Uuid::now_v7())
}
#[derive(Default)]
pub struct InMemoryMemoryProvenanceStore {
entries: Mutex<Vec<MemoryProvenanceEntry>>,
}
impl InMemoryMemoryProvenanceStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
pub(crate) fn tamper_entry_hash(
&self,
entry_id: &str,
forged_hash: &str,
) -> Result<MemoryProvenanceEntry, MemoryProvenanceError> {
let mut guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
for entry in guard.iter_mut() {
if entry.entry_id == entry_id {
let previous = entry.clone();
entry.hash = forged_hash.to_string();
return Ok(previous);
}
}
Err(MemoryProvenanceError::NotFound(entry_id.to_string()))
}
}
impl MemoryProvenanceStore for InMemoryMemoryProvenanceStore {
fn append(
&self,
input: MemoryProvenanceAppend,
) -> Result<MemoryProvenanceEntry, MemoryProvenanceError> {
let mut guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
let prev_hash = guard
.last()
.map(|entry| entry.hash.clone())
.unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string());
let entry_id = next_entry_id();
let hash = recompute_entry_hash(
&entry_id,
&input.store,
&input.key,
&input.capability_id,
&input.receipt_id,
input.written_at,
&prev_hash,
)?;
let entry = MemoryProvenanceEntry {
entry_id,
store: input.store,
key: input.key,
capability_id: input.capability_id,
receipt_id: input.receipt_id,
written_at: input.written_at,
prev_hash,
hash,
};
guard.push(entry.clone());
Ok(entry)
}
fn get_entry(
&self,
entry_id: &str,
) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError> {
let guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
Ok(guard
.iter()
.find(|entry| entry.entry_id == entry_id)
.cloned())
}
fn latest_for_key(
&self,
store: &str,
key: &str,
) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError> {
let guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
Ok(guard
.iter()
.rev()
.find(|entry| entry.store == store && entry.key == key)
.cloned())
}
fn verify_entry(
&self,
entry_id: &str,
) -> Result<ProvenanceVerification, MemoryProvenanceError> {
let guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
let Some(index) = guard.iter().position(|entry| entry.entry_id == entry_id) else {
return Ok(ProvenanceVerification::Unverified {
reason: UnverifiedReason::NoProvenance,
});
};
let entry = &guard[index];
let expected = entry.expected_hash()?;
if expected != entry.hash {
return Ok(ProvenanceVerification::Unverified {
reason: UnverifiedReason::ChainTampered,
});
}
let expected_prev = if index == 0 {
MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string()
} else {
guard[index - 1].hash.clone()
};
if expected_prev != entry.prev_hash {
return Ok(ProvenanceVerification::Unverified {
reason: UnverifiedReason::ChainLinkBroken,
});
}
let chain_digest = guard
.last()
.map(|tail| tail.hash.clone())
.unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string());
Ok(ProvenanceVerification::Verified {
entry: entry.clone(),
chain_digest,
})
}
fn chain_digest(&self) -> Result<String, MemoryProvenanceError> {
let guard = self
.entries
.lock()
.map_err(|_| MemoryProvenanceError::Backend("entries mutex poisoned".to_string()))?;
Ok(guard
.last()
.map(|entry| entry.hash.clone())
.unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryActionKind {
Write { store: String, key: String },
Read { store: String, key: String },
}
#[must_use]
pub fn classify_memory_action(
tool_name: &str,
arguments: &serde_json::Value,
) -> Option<MemoryActionKind> {
let tool = tool_name.to_ascii_lowercase();
if is_memory_write_tool_name(&tool) {
let (store, key) = extract_store_and_key(&tool, arguments);
return Some(MemoryActionKind::Write { store, key });
}
if is_memory_read_tool_name(&tool) {
let (store, key) = extract_store_and_key(&tool, arguments);
return Some(MemoryActionKind::Read { store, key });
}
None
}
fn is_memory_write_tool_name(tool: &str) -> bool {
matches!(
tool,
"memory_write"
| "remember"
| "store_memory"
| "vector_upsert"
| "vector_write"
| "upsert"
| "pinecone_upsert"
| "weaviate_write"
| "qdrant_upsert"
)
}
fn is_memory_read_tool_name(tool: &str) -> bool {
matches!(
tool,
"memory_read"
| "recall"
| "retrieve_memory"
| "vector_query"
| "vector_search"
| "similarity_search"
| "pinecone_query"
| "weaviate_search"
| "qdrant_search"
)
}
fn extract_store_and_key(tool: &str, arguments: &serde_json::Value) -> (String, String) {
let store = arguments
.get("collection")
.or_else(|| arguments.get("index"))
.or_else(|| arguments.get("namespace"))
.or_else(|| arguments.get("store"))
.and_then(|value| value.as_str())
.map(str::to_string)
.unwrap_or_else(|| tool.to_string());
let key = arguments
.get("id")
.or_else(|| arguments.get("key"))
.or_else(|| arguments.get("memory_id"))
.and_then(|value| value.as_str())
.map(str::to_string)
.unwrap_or_default();
(store, key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_assigns_genesis_prev_hash_and_hex_hash() {
let store = InMemoryMemoryProvenanceStore::new();
let entry = store
.append(MemoryProvenanceAppend {
store: "vector:rag-notes".into(),
key: "doc-1".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-1".into(),
written_at: 100,
})
.expect("append succeeds");
assert_eq!(entry.prev_hash, MEMORY_PROVENANCE_GENESIS_PREV_HASH);
assert_eq!(entry.hash.len(), 64);
assert!(entry.hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn append_links_successive_entries_via_prev_hash() {
let store = InMemoryMemoryProvenanceStore::new();
let first = store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "a".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-1".into(),
written_at: 100,
})
.unwrap();
let second = store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "b".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-2".into(),
written_at: 101,
})
.unwrap();
assert_eq!(second.prev_hash, first.hash);
assert_ne!(second.hash, first.hash);
}
#[test]
fn latest_for_key_returns_most_recent_entry() {
let store = InMemoryMemoryProvenanceStore::new();
store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "doc-1".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-1".into(),
written_at: 100,
})
.unwrap();
let later = store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "doc-1".into(),
capability_id: "cap-2".into(),
receipt_id: "rcpt-2".into(),
written_at: 150,
})
.unwrap();
let latest = store
.latest_for_key("s", "doc-1")
.unwrap()
.expect("an entry for doc-1 should exist");
assert_eq!(latest.entry_id, later.entry_id);
assert_eq!(latest.capability_id, "cap-2");
}
#[test]
fn verify_entry_detects_hash_tamper() {
let store = InMemoryMemoryProvenanceStore::new();
let entry = store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "doc-1".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-1".into(),
written_at: 100,
})
.unwrap();
let forged = "f".repeat(64);
store
.tamper_entry_hash(&entry.entry_id, &forged)
.expect("test helper should overwrite the entry");
let verification = store.verify_entry(&entry.entry_id).unwrap();
assert!(
matches!(
verification,
ProvenanceVerification::Unverified {
reason: UnverifiedReason::ChainTampered
}
),
"expected chain_tampered verification, got {verification:?}"
);
}
#[test]
fn verify_entry_flags_unverified_when_id_absent() {
let store = InMemoryMemoryProvenanceStore::new();
let verification = store.verify_entry("missing-id").unwrap();
assert!(matches!(
verification,
ProvenanceVerification::Unverified {
reason: UnverifiedReason::NoProvenance
}
));
}
#[test]
fn classify_memory_action_detects_writes_and_reads() {
let args = serde_json::json!({"collection": "notes", "id": "doc-42"});
match classify_memory_action("memory_write", &args) {
Some(MemoryActionKind::Write { store, key }) => {
assert_eq!(store, "notes");
assert_eq!(key, "doc-42");
}
other => panic!("expected MemoryActionKind::Write, got {other:?}"),
}
match classify_memory_action("vector_query", &args) {
Some(MemoryActionKind::Read { store, key }) => {
assert_eq!(store, "notes");
assert_eq!(key, "doc-42");
}
other => panic!("expected MemoryActionKind::Read, got {other:?}"),
}
assert!(classify_memory_action("read_file", &args).is_none());
}
#[test]
fn chain_digest_matches_tail_hash() {
let store = InMemoryMemoryProvenanceStore::new();
assert_eq!(
store.chain_digest().unwrap(),
MEMORY_PROVENANCE_GENESIS_PREV_HASH
);
let entry = store
.append(MemoryProvenanceAppend {
store: "s".into(),
key: "k".into(),
capability_id: "cap-1".into(),
receipt_id: "rcpt-1".into(),
written_at: 10,
})
.unwrap();
assert_eq!(store.chain_digest().unwrap(), entry.hash);
}
}