1use super::{AtomicDraftStore, DraftError};
4use lys_pass::drafts::{ApprovedDraft, Execution};
5
6use super::waiting::durable::{DraftJournal, Outcome};
7use super::{ApprovalEvidence, ApprovalTransition, execution_digest, same_request};
8use crate::agents::{error::AgentError, runtime::Agents};
9use crate::services::lifecycle::site::executor::OutcomeDelivery;
10use std::sync::Arc;
11
12pub struct AgentDraftStore {
14 pub(super) journal: Arc<DraftJournal>,
15 pub(super) agents: Arc<Agents>,
16 pub(super) delivery: Arc<dyn OutcomeDelivery>,
17 pub(super) projection: Arc<dyn super::AgentProjection>,
18}
19impl AgentDraftStore {
20 #[must_use]
22 pub fn new(
23 owners: &super::waiting::durable::ApprovalAgents,
24 delivery: Arc<dyn OutcomeDelivery>,
25 ) -> Self {
26 Self {
27 journal: owners.journal().clone(),
28 agents: owners.agents().clone(),
29 delivery,
30 projection: owners.agents().clone(),
31 }
32 }
33 fn commit_permission(
34 &self,
35 draft: &ApprovedDraft,
36 approval: &ApprovalTransition,
37 ) -> Result<Execution, DraftError> {
38 let record = self
39 .agents
40 .registry()
41 .record(&draft.target.id)
42 .map_err(|_| DraftError::Contract)?;
43 if !record.active() {
44 return Err(DraftError::Contract);
45 }
46 crate::auth_lys::prepared::validate_permission_words(&draft.words)
47 .map_err(|_| DraftError::Contract)?;
48 let receipt = Execution::Executed {
49 request_digest: draft.request_digest.clone(),
50 receipt_digest: lys_pass::drafts::request_digest(&format!(
51 "{}:{}",
52 draft.id, draft.request_digest
53 )),
54 };
55 self.journal.update(|snapshot| {
56 snapshot.outcomes.insert(
57 draft.id.clone(),
58 Outcome {
59 draft: Some(draft.clone()),
60 approval: approval.clone(),
61 receipt: receipt.clone(),
62 acknowledged: false,
63 delivered: false,
64 },
65 );
66 Ok(receipt)
67 })
68 }
69}
70impl AtomicDraftStore for AgentDraftStore {
71 fn receipt(&self, id: &str) -> Result<Option<Execution>, DraftError> {
72 self.journal.read(|snapshot| {
73 snapshot
74 .outcomes
75 .get(id)
76 .map(|outcome| outcome.receipt.clone())
77 })
78 }
79 fn approval(&self, draft: &ApprovedDraft) -> Result<ApprovalEvidence, DraftError> {
80 super::issuer_approval(&self.journal, draft)
81 }
82 fn commit(
83 &mut self,
84 draft: &ApprovedDraft,
85 approval: &ApprovalTransition,
86 ) -> Result<Execution, DraftError> {
87 if let Some(receipt) = self.receipt(&draft.id)? {
88 return Ok(receipt);
89 }
90 let prepared = self.journal.read(|snapshot| {
91 snapshot
92 .prepared
93 .values()
94 .find(|prepared| prepared.draft.as_deref() == Some(&draft.id))
95 .cloned()
96 })?;
97 let invalid = prepared
98 .as_ref()
99 .is_none_or(|prepared| !same_request(&prepared.request, draft));
100 if !invalid
101 && draft.target.kind == "liminal.agent"
102 && draft.target.action == "agent_permission"
103 {
104 return self.commit_permission(draft, approval);
105 }
106 let receipt = if invalid {
107 Execution::RefusedOnExecution {
108 request_digest: draft.request_digest.clone(),
109 refusal: "draft_preparation_mismatch".into(),
110 reason: "approved words do not match a durable prepared operation".into(),
111 }
112 } else {
113 if let Err(error) = self.journal.begin(draft, approval) {
114 if !matches!(error, DraftError::Contract | DraftError::Bound) {
115 return Err(error);
116 }
117 let refusal = Execution::RefusedOnExecution {
118 request_digest: draft.request_digest.clone(),
119 refusal: error.to_string(),
120 reason: "prepared operation no longer validates within its target bounds"
121 .into(),
122 };
123 return self.journal.update(|snapshot| {
124 snapshot.outcomes.insert(
125 draft.id.clone(),
126 Outcome {
127 draft: Some(draft.clone()),
128 approval: approval.clone(),
129 receipt: refusal.clone(),
130 acknowledged: false,
131 delivered: false,
132 },
133 );
134 Ok(refusal)
135 });
136 }
137 let result = self
138 .agents
139 .registry()
140 .execute_approved_at(draft, approval.executed_at_ms);
141 self.journal.finish()?;
142 match result {
143 Ok(receipt) => {
144 if self.receipt(&draft.id)?.as_ref() != Some(&receipt) {
145 return Err(DraftError::Store);
146 }
147 return Ok(receipt);
148 }
149 Err(AgentError::Store(_) | AgentError::Uncertain | AgentError::Delivery(_)) => {
150 return Err(DraftError::Store);
151 }
152 Err(error) => Execution::RefusedOnExecution {
153 request_digest: draft.request_digest.clone(),
154 refusal: agent_refusal(&error).into(),
155 reason: error.to_string(),
156 },
157 }
158 };
159 self.journal.update(|snapshot| {
160 snapshot.outcomes.insert(
161 draft.id.clone(),
162 Outcome {
163 draft: Some(draft.clone()),
164 approval: approval.clone(),
165 receipt: receipt.clone(),
166 acknowledged: false,
167 delivered: false,
168 },
169 );
170 Ok(receipt)
171 })
172 }
173 fn acknowledged(&self, id: &str) -> Result<bool, DraftError> {
174 self.journal.read(|snapshot| {
175 snapshot
176 .outcomes
177 .get(id)
178 .is_some_and(|outcome| outcome.acknowledged)
179 })
180 }
181 fn acknowledge(&mut self, id: &str, receipt: &Execution) -> Result<(), DraftError> {
182 self.journal.update(|snapshot| {
183 let outcome = snapshot
184 .outcomes
185 .get_mut(id)
186 .ok_or(DraftError::ReceiptMismatch)?;
187 if &outcome.receipt != receipt {
188 return Err(DraftError::ReceiptMismatch);
189 }
190 outcome.acknowledged = true;
191 Ok(())
192 })
193 }
194 fn pending_acknowledgments(
195 &self,
196 limit: std::num::NonZeroUsize,
197 ) -> Result<Vec<ApprovedDraft>, DraftError> {
198 self.journal.read(|snapshot| {
199 snapshot
200 .outcomes
201 .values()
202 .filter(|outcome| !outcome.acknowledged)
203 .filter_map(|outcome| outcome.draft.clone())
204 .take(limit.get())
205 .collect()
206 })
207 }
208 fn deliver_outcome(
209 &mut self,
210 draft: &ApprovedDraft,
211 receipt: &Execution,
212 ) -> Result<(), DraftError> {
213 let outcome = self
214 .journal
215 .read(|snapshot| snapshot.outcomes.get(&draft.id).cloned())?
216 .ok_or(DraftError::ReceiptMismatch)?;
217 if &outcome.receipt != receipt || execution_digest(receipt) != draft.request_digest {
218 return Err(DraftError::ReceiptMismatch);
219 }
220 if outcome.delivered {
221 return Ok(());
222 }
223 let holder = self.journal.read(|snapshot| {
224 snapshot
225 .prepared
226 .values()
227 .find(|prepared| prepared.draft.as_deref() == Some(&draft.id))
228 .map(|prepared| prepared.holder.clone())
229 })?;
230 if let Some(holder) = holder {
231 self.delivery.deliver(&holder, draft, receipt)?;
232 }
233 if matches!(receipt, Execution::Executed { .. }) {
234 self.projection.apply(draft)?;
235 }
236 self.journal.update(|snapshot| {
237 let outcome = snapshot
238 .outcomes
239 .get_mut(&draft.id)
240 .ok_or(DraftError::Store)?;
241 outcome.draft = Some(draft.clone());
242 outcome.delivered = true;
243 Ok(())
244 })
245 }
246}
247
248mod journal_store {
249 use super::super::waiting::durable::DraftJournal;
250 use crate::agents::registry::REGISTRY_STREAM;
251 use crate::auth_lys::live::drafts::DraftError;
252 use liminal::durability::{
253 DurabilityError, DurableStore, RoutedBatchEntry, RoutedBatchItem, ScanCursor, ScanPage,
254 StoredEntry,
255 };
256 use std::sync::Arc;
257 #[async_trait::async_trait]
258 impl DurableStore for DraftJournal {
259 fn max_request_bytes(&self) -> Option<usize> {
260 self.store.max_request_bytes()
261 }
262 fn reclaim_spine(&self) -> Option<Arc<haematite::EventStore>> {
263 self.store.reclaim_spine()
264 }
265 fn store_counts(&self) -> Option<liminal::durability::StoreCountsHandle> {
266 self.store.store_counts()
267 }
268 async fn apply_routed_batch(
269 &self,
270 route: &str,
271 items: Vec<RoutedBatchItem>,
272 ) -> Result<(), DurabilityError> {
273 self.store.apply_routed_batch(route, items).await
274 }
275 async fn apply_routed_entries(
276 &self,
277 route: &str,
278 entries: Vec<RoutedBatchEntry>,
279 ) -> Result<(), DurabilityError> {
280 self.store.apply_routed_entries(route, entries).await
281 }
282 async fn get_routed(
283 &self,
284 route: &str,
285 key: &[u8],
286 ) -> Result<Option<Vec<u8>>, DurabilityError> {
287 self.store.get_routed(route, key).await
288 }
289 async fn append(
290 &self,
291 stream: &str,
292 payload: Vec<u8>,
293 expected: u64,
294 ) -> Result<u64, DurabilityError> {
295 if stream == REGISTRY_STREAM {
296 self.append_native(vec![payload], expected)
297 .map_err(|error| Self::storage(&error))?;
298 Ok(expected)
299 } else {
300 self.store.append(stream, payload, expected).await
301 }
302 }
303 async fn append_all(
304 &self,
305 stream: &str,
306 payloads: Vec<Vec<u8>>,
307 expected: u64,
308 ) -> Result<u64, DurabilityError> {
309 if stream == REGISTRY_STREAM {
310 self.append_native(payloads, expected)
311 .map_err(|error| Self::storage(&error))
312 } else {
313 self.store.append_all(stream, payloads, expected).await
314 }
315 }
316 async fn read_from(
317 &self,
318 stream: &str,
319 offset: u64,
320 limit: usize,
321 ) -> Result<Vec<StoredEntry>, DurabilityError> {
322 if stream != REGISTRY_STREAM {
323 return self.store.read_from(stream, offset, limit).await;
324 }
325 self.read(|snapshot| {
326 let offset = usize::try_from(offset).map_err(|_| DraftError::Bound)?;
327 snapshot
328 .rows
329 .iter()
330 .enumerate()
331 .skip(offset)
332 .take(limit)
333 .map(|(sequence, row)| {
334 Ok(StoredEntry {
335 payload: serde_json::to_vec(&row.target)
336 .map_err(|_| DraftError::Store)?,
337 sequence: u64::try_from(sequence).map_err(|_| DraftError::Bound)?,
338 timestamp: row.timestamp,
339 })
340 })
341 .collect::<Result<Vec<_>, DraftError>>()
342 })
343 .map_err(|error| Self::storage(&error))?
344 .map_err(|error| Self::storage(&error))
345 }
346 async fn cas(&self, key: &str, old: u64, new: u64) -> Result<(), DurabilityError> {
347 self.store.cas(key, old, new).await
348 }
349 async fn read_stream_next_seq(&self, stream: &str) -> Result<Option<u64>, DurabilityError> {
350 if stream != REGISTRY_STREAM {
351 return self.store.read_stream_next_seq(stream).await;
352 }
353 self.read(|snapshot| {
354 if snapshot.rows.is_empty() {
355 Ok(None)
356 } else {
357 u64::try_from(snapshot.rows.len())
358 .map(Some)
359 .map_err(|_| DraftError::Bound)
360 }
361 })
362 .map_err(|error| Self::storage(&error))?
363 .map_err(|error| Self::storage(&error))
364 }
365 async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
366 self.store.read_value(key).await
367 }
368 async fn scan_page(
369 &self,
370 prefix: &str,
371 cursor: Option<ScanCursor>,
372 limit: usize,
373 ) -> Result<ScanPage, DurabilityError> {
374 if REGISTRY_STREAM.starts_with(prefix) || prefix.starts_with(REGISTRY_STREAM) {
375 return Err(DurabilityError::ConfigError(
376 "agent approval history requires its exact stream reader".into(),
377 ));
378 }
379 self.store.scan_page(prefix, cursor, limit).await
380 }
381 async fn flush(&self) -> Result<(), DurabilityError> {
382 self.flush_pending()
383 }
384 async fn truncate_before(&self, stream: &str, before: u64) -> Result<u64, DurabilityError> {
385 if stream == REGISTRY_STREAM {
386 return Err(DurabilityError::TruncateUnsupported {
387 store: "DraftJournal",
388 stream_key: stream.into(),
389 before_seq: before,
390 });
391 }
392 self.store.truncate_before(stream, before).await
393 }
394 async fn scan_routed(
395 &self,
396 route: &str,
397 from: &[u8],
398 to: &[u8],
399 ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, DurabilityError> {
400 self.store.scan_routed(route, from, to).await
401 }
402 async fn remove_routed_range(
403 &self,
404 route: &str,
405 from: &[u8],
406 to: &[u8],
407 ) -> Result<u64, DurabilityError> {
408 self.store.remove_routed_range(route, from, to).await
409 }
410 }
411}
412
413impl super::TargetDraftStore {
414 pub(super) fn receipt_owner(&self, id: &str) -> Result<bool, DraftError> {
415 let agent = self.agents.receipt(id)?.is_some();
416 let site = self.sites.receipt(id)?.is_some();
417 if agent == site {
418 return Err(DraftError::ReceiptMismatch);
419 }
420 Ok(agent)
421 }
422}
423
424const fn agent_refusal(error: &AgentError) -> &'static str {
425 match error {
426 AgentError::Unknown(_) => "unknown_agent",
427 AgentError::NameTaken(_) => "agent_name_taken",
428 AgentError::Transition(_) => "agent_transition_refused",
429 AgentError::Invalid(_) => "agent_words_invalid",
430 AgentError::Bound { .. } => "agent_bound_exceeded",
431 AgentError::DraftMismatch => "agent_draft_digest_mismatch",
432 _ => "agent_execution_refused",
433 }
434}
435
436pub struct Kept {
437 pub(crate) snapshot: super::waiting::durable::Snapshot,
438 pub(crate) hash: Option<haematite::tree::Hash>,
439 pub(crate) uncertain: bool,
440 pub(crate) active: Option<(ApprovedDraft, ApprovalTransition)>,
441 pub(crate) pending: Option<(super::waiting::durable::Snapshot, haematite::tree::Hash)>,
442}
443
444#[cfg(test)]
445#[path = "grant_stream_tests.rs"]
446mod tests;