1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5
6use khive_types::{Details, ErrorKind, KhiveError};
7use uuid::Uuid;
8
9use crate::credentials::receipt_sealer::{ReceiptFields, ReceiptSealError, ReceiptSealer};
10use crate::credentials::{CredentialRegistry, VisibilityReceiptConfig};
11use crate::{KhiveRuntime, RuntimeConfig, RuntimeError, RuntimeResult};
12
13const MAX_AGE_MS: i64 = 24 * 60 * 60 * 1_000;
14const MAX_FUTURE_MS: i64 = 5 * 60 * 1_000;
15const REPLAY_PHASE: &str = "exact_replay";
16
17pub(crate) struct ReceiptCutover {
21 backend: Arc<khive_db::StorageBackend>,
22 ready: AtomicBool,
23 check: parking_lot::Mutex<()>,
24}
25
26impl ReceiptCutover {
27 pub(crate) fn new(backend: Arc<khive_db::StorageBackend>, validated: bool) -> Self {
28 Self {
29 backend,
30 ready: AtomicBool::new(validated),
31 check: parking_lot::Mutex::new(()),
32 }
33 }
34
35 pub(crate) fn is_bound_to(&self, backend: &Arc<khive_db::StorageBackend>) -> bool {
36 Arc::ptr_eq(&self.backend, backend)
37 }
38
39 fn ensure(&self) -> RuntimeResult<()> {
40 if self.ready.load(Ordering::Acquire) {
41 return Ok(());
42 }
43 let _check = self.check.lock();
44 if self.ready.load(Ordering::Acquire) {
45 return Ok(());
46 }
47 self.backend
48 .validate_memory_visibility_cutover()
49 .map_err(|_| receipt_failure("receipt_store_unavailable", None, false))?;
50 self.ready.store(true, Ordering::Release);
51 Ok(())
52 }
53
54 fn recheck(&self) -> RuntimeResult<()> {
58 let _check = self.check.lock();
59 if self.backend.validate_memory_visibility_cutover().is_ok() {
60 return Ok(());
61 }
62 self.ready.store(false, Ordering::Release);
63 Err(receipt_failure("receipt_store_unavailable", None, false))
64 }
65}
66
67pub(crate) enum ReceiptCapability {
68 Absent,
69 Configured(ReceiptSealer),
70 Unavailable,
71}
72
73impl ReceiptCapability {
74 pub(crate) fn from_config(config: &RuntimeConfig) -> Self {
75 let Some(ring) = &config.visibility_receipts else {
76 return Self::Absent;
77 };
78 let Ok(registry) = CredentialRegistry::new(config.credentials.clone()) else {
79 return Self::Unavailable;
80 };
81 match ReceiptSealer::new(ring.clone(), Arc::new(registry)) {
82 Ok(sealer) => Self::Configured(sealer),
83 Err(_) => Self::Unavailable,
84 }
85 }
86
87 fn sealer(&self) -> RuntimeResult<&ReceiptSealer> {
88 match self {
89 Self::Configured(sealer) => Ok(sealer),
90 Self::Absent | Self::Unavailable => {
91 Err(receipt_failure("visibility_key_unavailable", None, false))
92 }
93 }
94 }
95}
96
97pub struct AuthenticatedVisibilityReceipt(ReceiptFields);
100
101impl AuthenticatedVisibilityReceipt {
102 pub fn namespace(&self) -> &str {
103 &self.0.namespace
104 }
105
106 pub fn sequence_for_model(&self, model: &str) -> Option<u64> {
107 self.0
108 .fences
109 .iter()
110 .find(|(name, _)| name == model)
111 .map(|(_, seq)| *seq)
112 }
113}
114
115fn invalid_receipt() -> RuntimeError {
116 RuntimeError::InvalidInput("memory.recall invalid visibility receipt".into())
117}
118
119fn seal_error(error: ReceiptSealError) -> RuntimeError {
120 match error {
121 ReceiptSealError::KeyUnavailable => {
122 receipt_failure("visibility_key_unavailable", None, false)
123 }
124 ReceiptSealError::NonceUnavailable => {
125 receipt_failure("visibility_nonce_unavailable", None, false)
126 }
127 ReceiptSealError::InvalidReceipt => invalid_receipt(),
128 }
129}
130
131pub(crate) fn receipt_failure(
132 reason: &'static str,
133 memory_id: Option<Uuid>,
134 replay: bool,
135) -> RuntimeError {
136 let mut fields = vec![("reason", reason.to_owned())];
137 if let Some(id) = memory_id {
138 fields.push(("memory_id", id.to_string()));
139 }
140 if replay {
141 fields.push(("receipt_phase", REPLAY_PHASE.to_owned()));
142 }
143 KhiveError::unavailable("freshness_unmet: memory visibility receipt unavailable")
144 .with_details(Details::new_owned(fields))
145 .into()
146}
147
148pub(crate) fn receipt_error_projection(error: &RuntimeError) -> Option<(bool, bool)> {
150 let RuntimeError::Khive(error) = error else {
151 return None;
152 };
153 if error.kind() != ErrorKind::Unavailable {
154 return None;
155 }
156 let details = error.details()?;
157 let reason = details.get("reason")?;
158 let retryable = match reason {
159 "receipt_temporarily_unavailable"
160 | "receipt_store_unavailable"
161 | "visibility_key_unavailable"
162 | "visibility_nonce_unavailable"
163 | "visibility_receipt_unavailable" => true,
164 "legacy_receipt_absent" | "receipt_epoch_unknown" | "visibility_token_expired" => false,
165 _ => return None,
166 };
167 let exact_replay = matches!(
168 reason,
169 "receipt_temporarily_unavailable" | "legacy_receipt_absent" | "receipt_epoch_unknown"
170 ) && details.get("receipt_phase") == Some(REPLAY_PHASE)
171 && details
172 .get("memory_id")
173 .is_some_and(|id| Uuid::parse_str(id).is_ok());
174 Some((retryable, exact_replay))
175}
176
177fn validate_fields(
178 fields: ReceiptFields,
179 namespaces: &[&str],
180 models: &[String],
181 now: i64,
182) -> RuntimeResult<AuthenticatedVisibilityReceipt> {
183 if !namespaces.contains(&fields.namespace.as_str())
184 || fields.fences.iter().any(|(name, _)| !models.contains(name))
185 {
186 return Err(invalid_receipt());
187 }
188 if fields.issued_at > now.saturating_add(MAX_FUTURE_MS) {
189 return Err(invalid_receipt());
190 }
191 if fields.issued_at < now.saturating_sub(MAX_AGE_MS) {
192 return Err(receipt_failure("visibility_token_expired", None, false));
193 }
194 Ok(AuthenticatedVisibilityReceipt(fields))
195}
196
197impl KhiveRuntime {
198 pub fn visibility_receipt_custody_notice(&self) -> Option<&'static str> {
205 match self.visibility_receipts.as_ref() {
206 ReceiptCapability::Absent => Some(
207 "no [visibility_receipts] section is configured: memory.remember stores memories \
208 without a visibility token and session recall refuses until receipt keys are \
209 configured",
210 ),
211 ReceiptCapability::Unavailable => Some(
212 "configured [visibility_receipts] custody is unusable: memory.remember and \
213 session recall refuse with visibility_key_unavailable; check configuration",
214 ),
215 ReceiptCapability::Configured(_) => None,
216 }
217 }
218
219 pub fn with_visibility_receipt_credentials(
222 mut self,
223 ring: VisibilityReceiptConfig,
224 credentials: Arc<CredentialRegistry>,
225 ) -> RuntimeResult<Self> {
226 let declarations = credentials.declarations();
227 let sealer = ReceiptSealer::new(ring.clone(), credentials).map_err(seal_error)?;
228 self.install_visibility_receipt_capability(
229 ring,
230 declarations,
231 ReceiptCapability::Configured(sealer),
232 );
233 Ok(self)
234 }
235
236 pub(crate) fn require_visibility_cutover(&self) -> RuntimeResult<()> {
237 self.visibility_cutover.ensure()
238 }
239
240 pub(crate) fn recheck_visibility_cutover(&self) -> RuntimeResult<()> {
242 self.visibility_cutover.recheck()
243 }
244
245 pub fn ensure_visibility_receipt_key(&self) -> RuntimeResult<()> {
247 self.require_visibility_cutover()?;
248 self.visibility_receipts
249 .sealer()?
250 .ensure_key()
251 .map_err(seal_error)
252 }
253
254 pub fn ensure_visibility_receipt_key_if_configured(&self) -> RuntimeResult<bool> {
260 self.require_visibility_cutover()?;
261 if matches!(*self.visibility_receipts, ReceiptCapability::Absent) {
262 return Ok(false);
263 }
264 self.visibility_receipts
265 .sealer()?
266 .ensure_key()
267 .map_err(seal_error)?;
268 Ok(true)
269 }
270
271 pub fn seal_visibility_receipt(
273 &self,
274 namespace: &str,
275 fences: &[(String, u64)],
276 ) -> RuntimeResult<String> {
277 self.require_visibility_cutover()?;
278 self.visibility_receipts
279 .sealer()?
280 .seal(namespace, fences)
281 .map_err(seal_error)
282 }
283
284 pub fn open_visibility_receipt(
286 &self,
287 token: &str,
288 effective_namespaces: &[&str],
289 requested_models: &[String],
290 ) -> RuntimeResult<AuthenticatedVisibilityReceipt> {
291 self.open_visibility_receipt_at(
292 token,
293 effective_namespaces,
294 requested_models,
295 chrono::Utc::now().timestamp_millis(),
296 )
297 }
298
299 fn open_visibility_receipt_at(
300 &self,
301 token: &str,
302 effective_namespaces: &[&str],
303 requested_models: &[String],
304 now: i64,
305 ) -> RuntimeResult<AuthenticatedVisibilityReceipt> {
306 self.require_visibility_cutover()?;
307 ReceiptSealer::validate_envelope(token).map_err(seal_error)?;
308 let fields = self
309 .visibility_receipts
310 .sealer()?
311 .open(token)
312 .map_err(seal_error)?;
313 validate_fields(fields, effective_namespaces, requested_models, now)
314 }
315}
316
317#[cfg(test)]
318#[path = "visibility_receipts_tests.rs"]
319mod tests;