Skip to main content

liminal_server/auth_lys/
grant_stream.rs

1//! Explicit startup, stream and console pulls share one serialized executor.
2
3use 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
12/// The native agent owner applies its transition through the approval journal.
13pub 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    /// Uses the registry that was opened over this exact approval journal.
21    #[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;