1use super::WaitingRecord;
4use crate::agents::registry::REGISTRY_STREAM;
5use crate::auth_lys::live::drafts::{ApprovalTransition, DraftError, execution_digest};
6use haematite::tree::Hash;
7use liminal::durability::{
8 DurabilityError, DurableStore, RoutedBatchItem, ScanCursor, bridge::block_on,
9};
10use lys_pass::drafts::{ApprovedDraft, DraftRequest, Execution};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::BTreeMap;
14use std::sync::{Arc, Mutex, MutexGuard};
15
16const ROUTE: &str = "liminal/approvals/v2";
17const KEY: &[u8] = b"snapshot";
18
19pub use crate::auth_lys::live::drafts::JournalLimits;
20
21#[derive(Clone, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct Prepared {
24 pub holder: String,
25 pub agent: Option<String>,
26 pub request: DraftRequest,
27 pub at_ms: u64,
28 pub mode: crate::auth_lys::GrantMode,
29 pub draft: Option<String>,
30}
31#[derive(Clone, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Outcome {
34 pub draft: Option<ApprovedDraft>,
35 pub approval: ApprovalTransition,
36 pub receipt: Execution,
37 pub acknowledged: bool,
38 pub delivered: bool,
39}
40#[derive(Clone, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct TargetRow {
43 pub(crate) timestamp: u64,
44 pub(crate) target: Value,
45 approval: Option<ApprovalTransition>,
46}
47#[derive(Clone, Default, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct Snapshot {
50 pub version: u8,
51 pub rows: Vec<TargetRow>,
52 pub prepared: BTreeMap<String, Prepared>,
53 pub outcomes: BTreeMap<String, Outcome>,
54 pub waiting: BTreeMap<String, WaitingRecord>,
55 pub deliveries: BTreeMap<String, super::Delivery>,
56}
57use super::super::pulls::Kept;
58
59pub struct DraftJournal {
62 pub(crate) store: Arc<dyn DurableStore>,
63 limits: JournalLimits,
64 kept: Mutex<Kept>,
65}
66impl std::fmt::Debug for DraftJournal {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_struct("DraftJournal").finish_non_exhaustive()
69 }
70}
71impl DraftJournal {
72 pub fn open(
78 store: Arc<dyn DurableStore>,
79 limits: JournalLimits,
80 ) -> Result<Arc<Self>, DraftError> {
81 let bytes = block_on(store.get_routed(ROUTE, KEY))
82 .map_err(|_| DraftError::Store)?
83 .map_err(|_| DraftError::Store)?;
84 let mut snapshot = Snapshot {
85 version: 2,
86 ..Snapshot::default()
87 };
88 if let Some(bytes) = &bytes {
89 if bytes.len() > limits.snapshot_bytes.get() {
90 return Err(DraftError::Bound);
91 }
92 snapshot = serde_json::from_slice(bytes).map_err(|_| DraftError::Store)?;
93 if snapshot.version != 2 {
94 return Err(DraftError::Contract);
95 }
96 } else {
97 Self::migrate(store.as_ref(), limits, &mut snapshot)?;
98 }
99 let journal = Arc::new(Self {
100 store,
101 limits,
102 kept: Mutex::new(Kept {
103 snapshot,
104 hash: bytes.as_deref().map(Hash::of),
105 uncertain: false,
106 active: None,
107 pending: None,
108 }),
109 });
110 journal.validate(&journal.lock()?.snapshot)?;
111 if bytes.is_none() {
112 journal.update(|_| Ok(()))?;
113 }
114 Ok(journal)
115 }
116 fn migrate(
117 store: &dyn DurableStore,
118 limits: JournalLimits,
119 snapshot: &mut Snapshot,
120 ) -> Result<(), DraftError> {
121 let head = block_on(store.read_stream_next_seq(REGISTRY_STREAM))
122 .map_err(|_| DraftError::Store)?
123 .map_err(|_| DraftError::Store)?
124 .unwrap_or(0);
125 if head > u64::try_from(limits.rows.get()).map_err(|_| DraftError::Bound)? {
126 return Err(DraftError::Bound);
127 }
128 let mut cursor = Some(ScanCursor {
129 stream_key: REGISTRY_STREAM.into(),
130 next_sequence: 0,
131 });
132 let mut next = 0;
133 let mut imported_bytes: usize = 0;
134 while next < head {
135 let page =
136 block_on(store.scan_page(REGISTRY_STREAM, cursor, limits.replay_page_rows.get()))
137 .map_err(|_| DraftError::Store)?
138 .map_err(|_| DraftError::Store)?;
139 if page.entries.is_empty() {
140 return Err(DraftError::Contract);
141 }
142 cursor = page.next;
143 for row in page.entries {
144 imported_bytes = imported_bytes
145 .checked_add(row.payload.len())
146 .ok_or(DraftError::Bound)?;
147 if imported_bytes > limits.snapshot_bytes.get() {
148 return Err(DraftError::Bound);
149 }
150 if row.sequence != next || next >= head {
151 return Err(DraftError::Contract);
152 }
153 let target: Value =
154 serde_json::from_slice(&row.payload).map_err(|_| DraftError::Store)?;
155 let approval = Self::legacy_approval(&target)?;
156 if let Some(approval) = &approval {
157 let receipt: Execution =
158 serde_json::from_value(target["receipt"]["execution"].clone())
159 .map_err(|_| DraftError::Store)?;
160 if snapshot
161 .outcomes
162 .insert(
163 approval.draft.clone(),
164 Outcome {
165 draft: None,
166 approval: approval.clone(),
167 receipt,
168 acknowledged: false,
169 delivered: false,
170 },
171 )
172 .is_some()
173 {
174 return Err(DraftError::Contract);
175 }
176 }
177 snapshot.rows.push(TargetRow {
178 timestamp: row.timestamp,
179 target,
180 approval,
181 });
182 next = next.checked_add(1).ok_or(DraftError::Bound)?;
183 }
184 }
185 if block_on(store.read_stream_next_seq(REGISTRY_STREAM))
186 .map_err(|_| DraftError::Store)?
187 .map_err(|_| DraftError::Store)?
188 .unwrap_or(0)
189 != head
190 {
191 return Err(DraftError::Contract);
192 }
193 Ok(())
194 }
195 fn legacy_approval(target: &Value) -> Result<Option<ApprovalTransition>, DraftError> {
196 if target["receipt"].is_null() {
197 return Ok(None);
198 }
199 let receipt: Execution = serde_json::from_value(target["receipt"]["execution"].clone())
200 .map_err(|_| DraftError::Store)?;
201 Ok(Some(ApprovalTransition {
202 draft: target["receipt"]["draft"]
203 .as_str()
204 .ok_or(DraftError::Contract)?
205 .into(),
206 digest: execution_digest(&receipt).into(),
207 approved_by: vec!["lys".into()],
208 executed_at_ms: target["transition"]["at_ms"]
209 .as_u64()
210 .ok_or(DraftError::Contract)?,
211 }))
212 }
213 pub(crate) fn lock(&self) -> Result<MutexGuard<'_, Kept>, DraftError> {
214 let kept = self.kept.lock().map_err(|_| DraftError::Store)?;
215 if kept.uncertain {
216 return Err(DraftError::Store);
217 }
218 Ok(kept)
219 }
220 fn validate(&self, snapshot: &Snapshot) -> Result<Vec<u8>, DraftError> {
221 if snapshot.rows.len() > self.limits.rows.get()
222 || snapshot.prepared.len() > self.limits.operations.get()
223 || snapshot.outcomes.len() > self.limits.operations.get()
224 || snapshot.deliveries.len() > self.limits.operations.get()
225 || snapshot.waiting.len() > self.limits.operations.get()
226 {
227 return Err(DraftError::Bound);
228 }
229 let bytes = serde_json::to_vec(snapshot).map_err(|_| DraftError::Store)?;
230 if bytes.len() > self.limits.snapshot_bytes.get()
231 || self
232 .store
233 .max_request_bytes()
234 .is_some_and(|bound| bytes.len() > bound)
235 {
236 return Err(DraftError::Bound);
237 }
238 Ok(bytes)
239 }
240 pub fn update<T>(
246 &self,
247 change: impl FnOnce(&mut Snapshot) -> Result<T, DraftError>,
248 ) -> Result<T, DraftError> {
249 self.replace(change, true)
250 }
251 fn replace<T>(
252 &self,
253 change: impl FnOnce(&mut Snapshot) -> Result<T, DraftError>,
254 flush: bool,
255 ) -> Result<T, DraftError> {
256 let mut kept = self.lock()?;
257 if kept.pending.is_some() || (flush && kept.active.is_some()) {
258 return Err(DraftError::Contract);
259 }
260 let mut candidate = kept.snapshot.clone();
261 let result = change(&mut candidate)?;
262 let bytes = self.validate(&candidate)?;
263 let hash = Hash::of(&bytes);
264 let write = block_on(self.store.apply_routed_batch(
265 ROUTE,
266 vec![RoutedBatchItem {
267 key: KEY.into(),
268 expected: kept.hash,
269 value: bytes,
270 ttl: None,
271 }],
272 ))
273 .map_err(|_| DraftError::Store)
274 .and_then(|result| result.map_err(|_| DraftError::Store));
275 if write.is_err() {
276 kept.uncertain = true;
277 return Err(DraftError::Store);
278 }
279 if flush {
280 if !matches!(block_on(self.store.flush()), Ok(Ok(()))) {
281 kept.uncertain = true;
282 return Err(DraftError::Store);
283 }
284 kept.snapshot = candidate;
285 kept.hash = Some(hash);
286 } else {
287 kept.pending = Some((candidate, hash));
288 }
289 drop(kept);
290 Ok(result)
291 }
292 pub fn read<T>(&self, read: impl FnOnce(&Snapshot) -> T) -> Result<T, DraftError> {
297 Ok(read(&self.lock()?.snapshot))
298 }
299 pub fn begin(
305 &self,
306 draft: &ApprovedDraft,
307 approval: &ApprovalTransition,
308 ) -> Result<(), DraftError> {
309 let mut kept = self.lock()?;
310 if approval.draft != draft.id
311 || approval.digest != draft.request_digest
312 || approval.approved_by.is_empty()
313 {
314 return Err(DraftError::ReceiptMismatch);
315 }
316 if kept.active.is_some() {
317 return Err(DraftError::Store);
318 }
319 let act: crate::agents::records::AgentAct =
320 serde_json::from_str(&draft.words).map_err(|_| DraftError::Contract)?;
321 let mut candidate = kept.snapshot.clone();
322 let receipt = Execution::Executed {
323 request_digest: draft.request_digest.clone(),
324 receipt_digest: lys_pass::drafts::request_digest(&format!(
325 "{}:{}:{}",
326 draft.id,
327 draft.request_digest,
328 candidate.rows.len()
329 )),
330 };
331 let target = serde_json::json!({"version":1,"name":act.name,"owner":act.owner,"transition":{"holder":act.holder,"at_ms":approval.executed_at_ms,"change":act.change},"receipt":{"draft":draft.id,"execution":receipt}});
332 candidate.rows.push(TargetRow {
333 timestamp: approval.executed_at_ms,
334 target,
335 approval: Some(approval.clone()),
336 });
337 candidate.outcomes.insert(
338 draft.id.clone(),
339 Outcome {
340 draft: Some(draft.clone()),
341 approval: approval.clone(),
342 receipt,
343 acknowledged: false,
344 delivered: false,
345 },
346 );
347 self.validate(&candidate)?;
348 kept.active = Some((draft.clone(), approval.clone()));
349 drop(kept);
350 Ok(())
351 }
352 pub fn finish(&self) -> Result<(), DraftError> {
357 self.kept.lock().map_err(|_| DraftError::Store)?.active = None;
358 Ok(())
359 }
360 pub(crate) fn append_native(
361 &self,
362 payloads: Vec<Vec<u8>>,
363 expected: u64,
364 ) -> Result<u64, DraftError> {
365 let active = self.lock()?.active.clone();
366 self.replace(
367 |snapshot| {
368 if u64::try_from(snapshot.rows.len()).map_err(|_| DraftError::Bound)? != expected
369 || payloads.is_empty()
370 {
371 return Err(DraftError::Contract);
372 }
373 for payload in payloads {
374 let target: Value =
375 serde_json::from_slice(&payload).map_err(|_| DraftError::Contract)?;
376 let approval = if target["receipt"].is_null() {
377 None
378 } else {
379 let (draft, transition) = active.as_ref().ok_or(DraftError::Contract)?;
380 let receipt: Execution =
381 serde_json::from_value(target["receipt"]["execution"].clone())
382 .map_err(|_| DraftError::Contract)?;
383 if target["receipt"]["draft"] != draft.id
384 || execution_digest(&receipt) != draft.request_digest
385 || target["name"] != draft.target.id
386 || target["transition"]["at_ms"] != transition.executed_at_ms
387 || snapshot.outcomes.contains_key(&draft.id)
388 {
389 return Err(DraftError::ReceiptMismatch);
390 }
391 snapshot.outcomes.insert(
392 draft.id.clone(),
393 Outcome {
394 draft: Some(draft.clone()),
395 approval: transition.clone(),
396 receipt,
397 acknowledged: false,
398 delivered: false,
399 },
400 );
401 Some(transition.clone())
402 };
403 snapshot.rows.push(TargetRow {
404 timestamp: target["transition"]["at_ms"]
405 .as_u64()
406 .ok_or(DraftError::Contract)?,
407 target,
408 approval,
409 });
410 }
411 u64::try_from(snapshot.rows.len()).map_err(|_| DraftError::Bound)
412 },
413 false,
414 )
415 }
416 pub(crate) fn flush_pending(&self) -> Result<(), DurabilityError> {
417 let mut kept = self.lock().map_err(|error| Self::storage(&error))?;
418 let result = block_on(self.store.flush())
419 .map_err(|_| Self::storage(&DraftError::Store))
420 .and_then(|result| result);
421 if let Err(error) = result {
422 kept.uncertain = true;
423 return Err(error);
424 }
425 if let Some((snapshot, hash)) = kept.pending.take() {
426 kept.snapshot = snapshot;
427 kept.hash = Some(hash);
428 }
429 drop(kept);
430 Ok(())
431 }
432 pub(crate) fn storage(error: &DraftError) -> DurabilityError {
433 DurabilityError::AppendUnresolved {
434 stream_key: REGISTRY_STREAM.into(),
435 sequence: 0,
436 cause: error.to_string(),
437 }
438 }
439}
440
441#[path = "waiting_agents.rs"]
442mod agents;
443pub use agents::{AgentBindings, ApprovalAgents};