Skip to main content

chio_kernel/
federation_artifact_store.rs

1//! Additive durable seam for bilateral co-sign artifacts. The kernel caches
2//! DualSignedReceipt / DsseEnvelope in a capped, idle-swept BoundedMap; when a
3//! FederationArtifactStore is configured, the co-sign hook writes through to it
4//! first, and accessors fall through to it on a cache miss.
5
6use 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    /// Whether a written artifact survives once the kernel's bounded front caches
22    /// evict it, so an evicted `id` is still resolvable through this store. Only a
23    /// persistent backend (database, object store) whose writes outlive eviction
24    /// returns `true`; a bounded, drop-evicting in-memory store returns `false`
25    /// because it can evict the same `id` the front caches did.
26    ///
27    /// The default is `false` so a store is never assumed durable unless it
28    /// explicitly guarantees it: treating a bounded cache as durable storage would
29    /// silently drop bilateral evidence with no operator signal.
30    fn is_durable(&self) -> bool {
31        false
32    }
33}
34
35/// Hard cap and idle sweep for [`InMemoryFederationArtifactStore`]'s backing
36/// maps. Mirrors the kernel's own federation cache defaults
37/// (`MemoryBudgetConfig::federation_cache_capacity`) so the reference store
38/// carries the same footprint discipline as the caches it sits behind.
39const IN_MEMORY_FEDERATION_ARTIFACT_STORE_CAPACITY: usize = 8192;
40const IN_MEMORY_FEDERATION_ARTIFACT_STORE_IDLE_TTL_SECS: u64 = 3600;
41
42/// Reference in-memory implementation (test double and default backing when a
43/// deployment wants durable bilateral evidence without a database). Capped,
44/// idle-swept, gauged instead of an unbounded HashMap: even though this store
45/// has no install seam today, it must not become a footgun once a follow-on
46/// wires one up. A deployment requiring persistence installs a database-backed
47/// impl instead.
48pub 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    /// Construct with an explicit per-map capacity and idle TTL. This is the seam
66    /// that lets a deployment wiring this reference store in from a CONFIGURED
67    /// process memory budget honor a lowered `federation_cache_capacity` instead of
68    /// the compiled-in 8192 default, so the store cannot exceed the operator's
69    /// configured federation memory budget even though the kernel's front caches
70    /// honor it. Same seam class as the velocity/journal `from_memory_budget`
71    /// constructors.
72    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    /// Construct from a CONFIGURED process memory budget so lowering
84    /// `federation_cache_capacity` / `federation_cache_idle_ttl_secs` actually
85    /// bounds each backing map, rather than every deployment retaining the
86    /// compiled-in 8192-entry default.
87    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    /// Live occupancy of the dual-signed-receipt backing map.
95    pub fn dual_signed_len(&self) -> usize {
96        self.dual_gauge.get()
97    }
98
99    /// Live occupancy of the DSSE-envelope backing map.
100    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        // Drop-oldest at capacity (BoundedMap's default eviction): the
113        // evicted entry, if any, is intentionally discarded here since this
114        // reference store has no durable tier to spill it to.
115        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        // The bundled in-memory store is a bounded, drop-evicting cache, so it must
221        // report itself NOT durable: an artifact evicted from the kernel's front
222        // caches is not guaranteed recoverable here, and claiming durability would
223        // silently drop bilateral evidence.
224        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        // The contract discriminates: a persistent backend overrides is_durable to
236        // advertise that evicted artifacts remain resolvable.
237        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        // Drop-oldest: the first-inserted id was evicted, the most recent survives.
284        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        // The CONFIGURED process memory budget must bound this reference store, not
292        // a hard-coded default. A budget lowering `federation_cache_capacity` to 4
293        // must cap EACH backing map at 4, not the compiled-in 8192; otherwise a
294        // deployment retains 8192 entries per map regardless of the configured
295        // budget.
296        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}