1use std::sync::Mutex;
7
8use chio_bounded::{BoundedMap, SizeGauge};
9use chio_federation::bilateral::DualSignedReceipt;
10use chio_federation::bilateral_dsse::DsseEnvelope;
11
12use crate::kernel::current_unix_timestamp;
13use crate::KernelError;
14
15pub trait FederationArtifactStore: Send + Sync {
16 fn put_dual_signed(&self, id: &str, receipt: &DualSignedReceipt) -> Result<(), KernelError>;
17 fn get_dual_signed(&self, id: &str) -> Result<Option<DualSignedReceipt>, KernelError>;
18 fn put_dsse(&self, id: &str, envelope: &DsseEnvelope) -> Result<(), KernelError>;
19 fn get_dsse(&self, id: &str) -> Result<Option<DsseEnvelope>, KernelError>;
20
21 fn is_durable(&self) -> bool {
31 false
32 }
33}
34
35const IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY: usize = 8192;
40const IN_MEMORY_FEDERATION_ARTIFACT_STORE_IDLE_TTL_SECS: u64 = 3600;
41
42pub struct InMemoryFederationArtifactStore {
49 dual: Mutex<BoundedMap<String, DualSignedReceipt>>,
50 dual_gauge: SizeGauge,
51 dsse: Mutex<BoundedMap<String, DsseEnvelope>>,
52 dsse_gauge: SizeGauge,
53}
54
55impl Default for InMemoryFederationArtifactStore {
56 fn default() -> Self {
57 Self::with_capacity(
58 IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY,
59 IN_MEMORY_FEDERATION_ARTIFACT_STORE_IDLE_TTL_SECS,
60 )
61 }
62}
63
64impl InMemoryFederationArtifactStore {
65 pub fn with_capacity(capacity: usize, idle_ttl_secs: u64) -> Self {
73 let dual_gauge = SizeGauge::new();
74 let dsse_gauge = SizeGauge::new();
75 Self {
76 dual: Mutex::new(BoundedMap::new(capacity, idle_ttl_secs, dual_gauge.clone())),
77 dual_gauge,
78 dsse: Mutex::new(BoundedMap::new(capacity, idle_ttl_secs, dsse_gauge.clone())),
79 dsse_gauge,
80 }
81 }
82
83 pub fn from_memory_budget(budget: &crate::MemoryBudgetConfig) -> Self {
88 Self::with_capacity(
89 budget.federation_cache_capacity,
90 budget.federation_cache_idle_ttl_secs,
91 )
92 }
93
94 pub fn dual_signed_len(&self) -> usize {
96 self.dual_gauge.get()
97 }
98
99 pub fn dsse_len(&self) -> usize {
101 self.dsse_gauge.get()
102 }
103}
104
105impl FederationArtifactStore for InMemoryFederationArtifactStore {
106 fn put_dual_signed(&self, id: &str, receipt: &DualSignedReceipt) -> Result<(), KernelError> {
107 let now = current_unix_timestamp();
108 let mut guard = match self.dual.lock() {
109 Ok(g) => g,
110 Err(poisoned) => poisoned.into_inner(),
111 };
112 let _evicted = guard.insert(id.to_string(), receipt.clone(), now);
116 Ok(())
117 }
118
119 fn get_dual_signed(&self, id: &str) -> Result<Option<DualSignedReceipt>, KernelError> {
120 let now = current_unix_timestamp();
121 let mut guard = match self.dual.lock() {
122 Ok(g) => g,
123 Err(poisoned) => poisoned.into_inner(),
124 };
125 Ok(guard.get(&id.to_string(), now).cloned())
126 }
127
128 fn put_dsse(&self, id: &str, envelope: &DsseEnvelope) -> Result<(), KernelError> {
129 let now = current_unix_timestamp();
130 let mut guard = match self.dsse.lock() {
131 Ok(g) => g,
132 Err(poisoned) => poisoned.into_inner(),
133 };
134 let _evicted = guard.insert(id.to_string(), envelope.clone(), now);
135 Ok(())
136 }
137
138 fn get_dsse(&self, id: &str) -> Result<Option<DsseEnvelope>, KernelError> {
139 let now = current_unix_timestamp();
140 let mut guard = match self.dsse.lock() {
141 Ok(g) => g,
142 Err(poisoned) => poisoned.into_inner(),
143 };
144 Ok(guard.get(&id.to_string(), now).cloned())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 #![allow(clippy::unwrap_used, clippy::expect_used)]
151 use super::*;
152
153 use chio_core::crypto::{Ed25519Backend, Keypair, SigningBackend};
154 use chio_core::receipt::{
155 body::ChioReceipt, body::ChioReceiptBody, decision::Decision, decision::ToolCallAction,
156 };
157 use chio_federation::bilateral_dsse::DsseSignature;
158
159 fn sample_dual_signed(id: &str) -> DualSignedReceipt {
160 let kp = Keypair::generate();
161 let body = ChioReceiptBody {
162 id: id.to_string(),
163 timestamp: 1_700_000_100,
164 capability_id: "cap-receipt".to_string(),
165 tool_server: "srv".to_string(),
166 tool_name: "echo".to_string(),
167 action: ToolCallAction::from_parameters(serde_json::json!({"message": "hello"}))
168 .expect("tool action"),
169 decision: Some(Decision::Allow),
170 receipt_kind: Default::default(),
171 boundary_class: Default::default(),
172 observation_outcome: None,
173 tool_origin: Default::default(),
174 redaction_mode: Default::default(),
175 actor_chain: Vec::new(),
176 content_hash: "0".repeat(64),
177 policy_hash: "1".repeat(64),
178 evidence: Vec::new(),
179 metadata: None,
180 trust_level: Default::default(),
181 tenant_id: None,
182 kernel_key: kp.public_key(),
183 bbs_projection_version: None,
184 };
185 let receipt = ChioReceipt::sign(body, &kp).expect("sign receipt");
186 let backend = Ed25519Backend::new(kp.clone());
187 let signature = backend.sign_bytes(id.as_bytes()).expect("sign bytes");
188 DualSignedReceipt {
189 schema: "test.dual-signed-receipt.v1".to_string(),
190 body: receipt,
191 org_a_kernel_id: "kernel.org-a".to_string(),
192 org_b_kernel_id: "kernel.org-b".to_string(),
193 org_a_signature: signature.clone(),
194 org_b_signature: signature,
195 }
196 }
197
198 fn sample_dsse(id: &str) -> DsseEnvelope {
199 let fixture_text = format!("payload-{id}");
200 DsseEnvelope {
201 payload_type: "application/vnd.in-toto+json".to_string(),
202 payload: fixture_text,
203 signatures: vec![DsseSignature {
204 keyid: format!("keyid-{id}"),
205 sig: "sig".to_string(),
206 }],
207 }
208 }
209
210 #[test]
211 fn trait_object_round_trips_absence_and_presence() {
212 let store: Box<dyn FederationArtifactStore> =
213 Box::new(InMemoryFederationArtifactStore::default());
214 assert!(store.get_dual_signed("missing").unwrap().is_none());
215 assert!(store.get_dsse("missing").unwrap().is_none());
216 }
217
218 #[test]
219 fn in_memory_store_reports_non_durable() {
220 let store = InMemoryFederationArtifactStore::default();
225 assert!(
226 !store.is_durable(),
227 "a bounded, drop-evicting store must not claim durability"
228 );
229 let as_trait: &dyn FederationArtifactStore = &store;
230 assert!(!as_trait.is_durable());
231 }
232
233 #[test]
234 fn durable_backend_can_opt_into_durability() {
235 struct DurableStub;
238 impl FederationArtifactStore for DurableStub {
239 fn put_dual_signed(
240 &self,
241 _id: &str,
242 _receipt: &DualSignedReceipt,
243 ) -> Result<(), KernelError> {
244 Ok(())
245 }
246 fn get_dual_signed(&self, _id: &str) -> Result<Option<DualSignedReceipt>, KernelError> {
247 Ok(None)
248 }
249 fn put_dsse(&self, _id: &str, _envelope: &DsseEnvelope) -> Result<(), KernelError> {
250 Ok(())
251 }
252 fn get_dsse(&self, _id: &str) -> Result<Option<DsseEnvelope>, KernelError> {
253 Ok(None)
254 }
255 fn is_durable(&self) -> bool {
256 true
257 }
258 }
259 let as_trait: &dyn FederationArtifactStore = &DurableStub;
260 assert!(as_trait.is_durable());
261 }
262
263 #[test]
264 fn dual_signed_backing_map_never_exceeds_cap_and_gauge_tracks_occupancy() {
265 let store = InMemoryFederationArtifactStore::default();
266 let inserts = IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY + 16;
267 for i in 0..inserts {
268 let id = format!("rcpt-{i}");
269 store
270 .put_dual_signed(&id, &sample_dual_signed(&id))
271 .unwrap();
272 assert!(
273 store.dual_signed_len() <= IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY,
274 "dual-signed backing map exceeded cap at insert {i}: len={}",
275 store.dual_signed_len()
276 );
277 }
278 assert_eq!(
279 store.dual_signed_len(),
280 IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY,
281 "backing map should be saturated at cap after over-filling"
282 );
283 assert!(store.get_dual_signed("rcpt-0").unwrap().is_none());
285 let last_id = format!("rcpt-{}", inserts - 1);
286 assert!(store.get_dual_signed(&last_id).unwrap().is_some());
287 }
288
289 #[test]
290 fn from_memory_budget_honors_lowered_federation_cache_capacity() {
291 let budget = crate::MemoryBudgetConfig {
297 federation_cache_capacity: 4,
298 ..crate::MemoryBudgetConfig::defaults()
299 };
300 let store = InMemoryFederationArtifactStore::from_memory_budget(&budget);
301
302 for i in 0..64 {
303 let id = format!("rcpt-{i}");
304 store
305 .put_dual_signed(&id, &sample_dual_signed(&id))
306 .unwrap();
307 assert!(
308 store.dual_signed_len() <= 4,
309 "configured federation_cache_capacity did not take effect: dual len={}",
310 store.dual_signed_len()
311 );
312 }
313 assert_eq!(
314 store.dual_signed_len(),
315 4,
316 "dual-signed backing map should saturate at the lowered cap"
317 );
318
319 for i in 0..64 {
320 let id = format!("dsse-{i}");
321 store.put_dsse(&id, &sample_dsse(&id)).unwrap();
322 assert!(
323 store.dsse_len() <= 4,
324 "configured federation_cache_capacity did not take effect: dsse len={}",
325 store.dsse_len()
326 );
327 }
328 assert_eq!(
329 store.dsse_len(),
330 4,
331 "dsse backing map should saturate at the lowered cap"
332 );
333 }
334
335 #[test]
336 fn dsse_backing_map_never_exceeds_cap_and_gauge_tracks_occupancy() {
337 let store = InMemoryFederationArtifactStore::default();
338 let inserts = IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY + 16;
339 for i in 0..inserts {
340 let id = format!("dsse-{i}");
341 store.put_dsse(&id, &sample_dsse(&id)).unwrap();
342 assert!(
343 store.dsse_len() <= IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY,
344 "dsse backing map exceeded cap at insert {i}: len={}",
345 store.dsse_len()
346 );
347 }
348 assert_eq!(
349 store.dsse_len(),
350 IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY,
351 "backing map should be saturated at cap after over-filling"
352 );
353 assert!(store.get_dsse("dsse-0").unwrap().is_none());
354 let last_id = format!("dsse-{}", inserts - 1);
355 assert!(store.get_dsse(&last_id).unwrap().is_some());
356 }
357}