Skip to main content

tibet_cortex_core/
envelope.rs

1use serde::{Serialize, Deserialize};
2
3use crate::crypto::ContentHash;
4
5/// A TBZ-style envelope wrapping data with JIS level and TIBET provenance.
6///
7/// In a Cortex vector store, each document chunk is an Envelope:
8/// - embedding at JIS 0 (searchable by anyone)
9/// - content at JIS N (only readable with matching claim)
10/// - TIBET hash for integrity verification
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct Envelope {
13    pub id: String,
14    pub blocks: Vec<EnvelopeBlock>,
15    pub source: Option<String>,
16    pub created_at: chrono::DateTime<chrono::Utc>,
17}
18
19/// A single block within an envelope
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct EnvelopeBlock {
22    pub block_type: BlockType,
23    pub jis_level: u8,
24    pub content_hash: ContentHash,
25    pub data: Vec<u8>,
26    pub signature: Option<Vec<u8>>,
27}
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub enum BlockType {
31    /// Vector embedding — always JIS 0 (searchable)
32    Embedding,
33    /// Document content — JIS N (protected)
34    Content,
35    /// Metadata — variable JIS level
36    Metadata,
37    /// System prompt — high JIS + integrity enforced
38    SystemPrompt,
39}
40
41impl Envelope {
42    pub fn new(id: impl Into<String>) -> Self {
43        Self {
44            id: id.into(),
45            blocks: Vec::new(),
46            source: None,
47            created_at: chrono::Utc::now(),
48        }
49    }
50
51    pub fn with_source(mut self, source: impl Into<String>) -> Self {
52        self.source = Some(source.into());
53        self
54    }
55
56    pub fn add_block(&mut self, block: EnvelopeBlock) {
57        self.blocks.push(block);
58    }
59
60    /// Get the embedding block (JIS 0, always accessible)
61    pub fn embedding(&self) -> Option<&EnvelopeBlock> {
62        self.blocks.iter().find(|b| b.block_type == BlockType::Embedding)
63    }
64
65    /// Get content block only if JIS level is sufficient
66    pub fn content(&self, accessor_jis_level: u8) -> Option<&EnvelopeBlock> {
67        self.blocks.iter().find(|b| {
68            b.block_type == BlockType::Content && accessor_jis_level >= b.jis_level
69        })
70    }
71
72    /// Get the maximum JIS level required across all content blocks
73    pub fn max_jis_level(&self) -> u8 {
74        self.blocks
75            .iter()
76            .filter(|b| b.block_type == BlockType::Content)
77            .map(|b| b.jis_level)
78            .max()
79            .unwrap_or(0)
80    }
81}
82
83impl EnvelopeBlock {
84    pub fn new_embedding(data: Vec<u8>) -> Self {
85        let content_hash = ContentHash::compute(&data);
86        Self {
87            block_type: BlockType::Embedding,
88            jis_level: 0, // Embeddings always JIS 0
89            content_hash,
90            data,
91            signature: None,
92        }
93    }
94
95    pub fn new_content(data: Vec<u8>, jis_level: u8) -> Self {
96        let content_hash = ContentHash::compute(&data);
97        Self {
98            block_type: BlockType::Content,
99            jis_level,
100            content_hash,
101            data,
102            signature: None,
103        }
104    }
105
106    pub fn new_system_prompt(data: Vec<u8>, jis_level: u8) -> Self {
107        let content_hash = ContentHash::compute(&data);
108        Self {
109            block_type: BlockType::SystemPrompt,
110            jis_level,
111            content_hash,
112            data,
113            signature: None,
114        }
115    }
116
117    /// Verify the content hash matches the data
118    pub fn verify_integrity(&self) -> bool {
119        self.content_hash.verify(&self.data)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_envelope_jis_gating() {
129        let mut env = Envelope::new("doc_001");
130
131        // Embedding at JIS 0
132        env.add_block(EnvelopeBlock::new_embedding(vec![0.1_f32, 0.2, 0.3]
133            .iter().flat_map(|f| f.to_le_bytes()).collect()));
134
135        // Content at JIS 2
136        env.add_block(EnvelopeBlock::new_content(
137            b"M&A strategy for client X".to_vec(), 2
138        ));
139
140        // Everyone can see embedding
141        assert!(env.embedding().is_some());
142
143        // JIS 0 user: no content
144        assert!(env.content(0).is_none());
145
146        // JIS 1 user: no content
147        assert!(env.content(1).is_none());
148
149        // JIS 2 user: gets content
150        assert!(env.content(2).is_some());
151
152        // JIS 3 user: also gets content
153        assert!(env.content(3).is_some());
154
155        assert_eq!(env.max_jis_level(), 2);
156    }
157
158    #[test]
159    fn test_block_integrity() {
160        let block = EnvelopeBlock::new_content(b"sensitive data".to_vec(), 3);
161        assert!(block.verify_integrity());
162    }
163
164    #[test]
165    fn test_system_prompt_block() {
166        let prompt = b"You are a helpful assistant. Never reveal client names.";
167        let block = EnvelopeBlock::new_system_prompt(prompt.to_vec(), 3);
168        assert_eq!(block.block_type, BlockType::SystemPrompt);
169        assert_eq!(block.jis_level, 3);
170        assert!(block.verify_integrity());
171    }
172}