Skip to main content

cortex_core/
tibet.rs

1use chrono::{DateTime, Utc};
2use serde::{Serialize, Deserialize};
3
4use crate::crypto::ContentHash;
5
6/// TIBET provenance token — who did what, when, why
7#[derive(Clone, Debug, Serialize, Deserialize)]
8pub struct TibetToken {
9    pub token_id: String,
10    pub parent_id: Option<String>,
11    pub timestamp: DateTime<Utc>,
12
13    // ERIN — what's IN the action
14    pub erin: ContentHash,
15
16    // ERAAN — what's attached (dependencies, references)
17    pub eraan: Vec<String>,
18
19    // EROMHEEN — context around it
20    pub eromheen: Eromheen,
21
22    // ERACHTER — intent behind it
23    pub erachter: String,
24
25    // Signature over the token
26    pub signature: Option<Vec<u8>>,
27}
28
29/// Context surrounding a TIBET action
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct Eromheen {
32    pub actor: String,
33    pub jis_level: u8,
34    pub chunks_accessed: usize,
35    pub chunks_denied: usize,
36    pub airlock_session_ms: Option<f64>,
37}
38
39/// Full provenance chain — append-only audit trail
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct Provenance {
42    pub chain: Vec<TibetToken>,
43}
44
45impl TibetToken {
46    pub fn new(
47        erin: ContentHash,
48        erachter: impl Into<String>,
49        actor: impl Into<String>,
50        jis_level: u8,
51    ) -> Self {
52        let id = format!("tibet_{}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
53        Self {
54            token_id: id,
55            parent_id: None,
56            timestamp: Utc::now(),
57            erin,
58            eraan: Vec::new(),
59            eromheen: Eromheen {
60                actor: actor.into(),
61                jis_level,
62                chunks_accessed: 0,
63                chunks_denied: 0,
64                airlock_session_ms: None,
65            },
66            erachter: erachter.into(),
67            signature: None,
68        }
69    }
70
71    pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
72        self.parent_id = Some(parent_id.into());
73        self
74    }
75
76    pub fn with_access_stats(mut self, accessed: usize, denied: usize) -> Self {
77        self.eromheen.chunks_accessed = accessed;
78        self.eromheen.chunks_denied = denied;
79        self
80    }
81
82    pub fn with_airlock_time(mut self, ms: f64) -> Self {
83        self.eromheen.airlock_session_ms = Some(ms);
84        self
85    }
86
87    /// Serialize the signable portion (everything except signature)
88    pub fn signable_bytes(&self) -> Vec<u8> {
89        let mut token = self.clone();
90        token.signature = None;
91        serde_json::to_vec(&token).unwrap_or_default()
92    }
93}
94
95impl Provenance {
96    pub fn new() -> Self {
97        Self { chain: Vec::new() }
98    }
99
100    pub fn append(&mut self, token: TibetToken) {
101        self.chain.push(token);
102    }
103
104    pub fn latest(&self) -> Option<&TibetToken> {
105        self.chain.last()
106    }
107
108    pub fn len(&self) -> usize {
109        self.chain.len()
110    }
111
112    pub fn is_empty(&self) -> bool {
113        self.chain.is_empty()
114    }
115
116    /// Verify the chain is unbroken: each token's parent_id matches the previous token_id
117    pub fn verify_chain(&self) -> bool {
118        for window in self.chain.windows(2) {
119            let parent = &window[0];
120            let child = &window[1];
121            match &child.parent_id {
122                Some(pid) if pid == &parent.token_id => continue,
123                _ => return false,
124            }
125        }
126        true
127    }
128}
129
130impl Default for Provenance {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_token_creation() {
142        let hash = ContentHash("sha256:abc123".into());
143        let token = TibetToken::new(hash, "test query", "user@test.com", 2);
144        assert!(token.token_id.starts_with("tibet_"));
145        assert_eq!(token.eromheen.jis_level, 2);
146        assert_eq!(token.erachter, "test query");
147    }
148
149    #[test]
150    fn test_provenance_chain() {
151        let h1 = ContentHash("sha256:aaa".into());
152        let h2 = ContentHash("sha256:bbb".into());
153
154        let t1 = TibetToken::new(h1, "first", "actor", 0);
155        let t1_id = t1.token_id.clone();
156        let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent(t1_id);
157
158        let mut prov = Provenance::new();
159        prov.append(t1);
160        prov.append(t2);
161
162        assert_eq!(prov.len(), 2);
163        assert!(prov.verify_chain());
164    }
165
166    #[test]
167    fn test_broken_chain() {
168        let h1 = ContentHash("sha256:aaa".into());
169        let h2 = ContentHash("sha256:bbb".into());
170
171        let t1 = TibetToken::new(h1, "first", "actor", 0);
172        let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent("wrong_parent");
173
174        let mut prov = Provenance::new();
175        prov.append(t1);
176        prov.append(t2);
177
178        assert!(!prov.verify_chain());
179    }
180}