Skip to main content

liminal_server/services/site/
executor.rs

1//! The construction target for the approved-draft executor and durable outbox.
2
3use lys_pass::drafts::{ApprovedDraft, DraftRequest, Execution};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6use std::num::NonZeroUsize;
7use std::sync::Arc;
8
9use super::super::Lifecycle;
10use super::{SiteAct, SiteError};
11use crate::auth_lys::live::drafts::{
12    ApprovalEvidence, ApprovalTransition, AtomicDraftStore, DraftError,
13};
14
15/// Immutable words kept only in their owning durable target store.
16#[derive(Clone, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub(in super::super) struct Prepared {
19    pub draft: ApprovedDraft,
20    pub act: SiteAct,
21    pub at_ms: u64,
22}
23
24/// The outcome and notification intent share the target's durable operation.
25#[derive(Clone, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub(in super::super) struct Outcome {
28    pub receipt: Execution,
29    pub approval: ApprovalTransition,
30    pub acknowledged: bool,
31    pub delivered: bool,
32}
33
34/// The approval owner authenticates the actual people and immutable words.
35pub trait ApprovalReader: Send + Sync {
36    /// Reads evidence for this exact draft from Lys's authenticated record.
37    ///
38    /// # Errors
39    /// Missing evidence or an issuer/storage refusal.
40    fn read(&self, draft: &ApprovedDraft) -> Result<ApprovalEvidence, DraftError>;
41}
42
43/// The agent owner delivers by immutable operation id through its state writer.
44pub trait OutcomeDelivery: Send + Sync {
45    /// Atomically clears waiting and puts one outcome in the bound agent inbox.
46    ///
47    /// # Errors
48    /// Missing agent binding, storage or publication failure; no guessed identity.
49    fn deliver(
50        &self,
51        holder: &str,
52        draft: &ApprovedDraft,
53        receipt: &Execution,
54    ) -> Result<(), DraftError>;
55}
56
57/// No process, timer or authority is created by this target adapter.
58pub struct SiteDraftStore {
59    lifecycle: Arc<Lifecycle>,
60    approvals: Arc<dyn ApprovalReader>,
61    delivery: Arc<dyn OutcomeDelivery>,
62    at_ms: u64,
63}
64
65impl SiteDraftStore {
66    /// Joins the existing approval reader and agent outcome owner.
67    #[must_use]
68    pub fn new(
69        lifecycle: Arc<Lifecycle>,
70        approvals: Arc<dyn ApprovalReader>,
71        delivery: Arc<dyn OutcomeDelivery>,
72        at_ms: u64,
73    ) -> Self {
74        Self {
75            lifecycle,
76            approvals,
77            delivery,
78            at_ms,
79        }
80    }
81
82    fn validate_approval(
83        &self,
84        draft: &ApprovedDraft,
85        transition: &ApprovalTransition,
86    ) -> Result<(), DraftError> {
87        let evidence = self.approvals.read(draft)?;
88        let needed = match evidence.mode {
89            crate::auth_lys::GrantMode::ByDraft => 1,
90            crate::auth_lys::GrantMode::ByTwo => 2,
91            crate::auth_lys::GrantMode::Outright => return Err(DraftError::ApprovalUnavailable),
92        };
93        let limits = &self
94            .lifecycle
95            .site_bindings()
96            .map_err(storage_error)?
97            .limits;
98        let mut people = BTreeSet::new();
99        if evidence.draft != draft.id
100            || evidence.digest != draft.request_digest
101            || evidence.approved_by != transition.approved_by
102            || evidence.approved_by.len() != needed
103            || evidence.approved_by.len() > limits.approvers.get()
104            || evidence.approved_by.iter().any(|person| {
105                person == "lys"
106                    || person.is_empty()
107                    || person.len() > limits.text_bytes.get()
108                    || person.chars().any(char::is_control)
109                    || !people.insert(person)
110            })
111        {
112            return Err(DraftError::ApprovalUnavailable);
113        }
114        Ok(())
115    }
116
117    /// Reads the target receipt through the approval owner's atomic interface.
118    ///
119    /// # Errors
120    /// The durable target ledger cannot be read.
121    pub fn receipt(&self, id: &str) -> Result<Option<Execution>, DraftError> {
122        AtomicDraftStore::receipt(self, id)
123    }
124
125    /// The operation clock is observed by the pulling owner, never the request.
126    pub const fn observe_execution_time(&mut self, at_ms: u64) {
127        self.at_ms = at_ms;
128    }
129
130    /// Validates every immutable field even on receipt replay, then delegates
131    /// execution and durable deduplication to Lys's shared executor contract.
132    ///
133    /// # Errors
134    /// A conflicting target, grant, digest, words or execution failure.
135    pub fn execute_approved(&mut self, draft: &ApprovedDraft) -> Result<Execution, DraftError> {
136        {
137            let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
138            let prepared = registry
139                .construction
140                .drafts
141                .get(&draft.id)
142                .ok_or(DraftError::Contract)?;
143            if !same_draft(&prepared.draft, draft) {
144                return Err(DraftError::ReceiptMismatch);
145            }
146            drop(registry);
147        }
148        let receipt = lys_pass::drafts::execute_once(draft, self).map_err(|error| match error {
149            lys_pass::drafts::ExecuteError::DigestMismatch => DraftError::ReceiptMismatch,
150            lys_pass::drafts::ExecuteError::Product(error) => error,
151            lys_pass::drafts::ExecuteError::Contract(_) => DraftError::Contract,
152        })?;
153        self.lifecycle.refresh_sites().map_err(storage_error)?;
154        Ok(receipt)
155    }
156}
157
158impl Lifecycle {
159    /// Records the exact prepared request after Lys durably holds it. Identity
160    /// is checked again against the original verified pass, never request fields.
161    ///
162    /// # Errors
163    /// Any immutable mismatch, invalid boundary or durable capacity failure.
164    pub fn keep_held_site(
165        &self,
166        id: &str,
167        request: &DraftRequest,
168        act: &SiteAct,
169        pass: &lys_pass::VerifiedPass,
170        mode: crate::auth_lys::GrantMode,
171        at_ms: u64,
172    ) -> Result<(), SiteError> {
173        let bindings = self.site_bindings()?;
174        let bound = bindings.limits.text_bytes.get();
175        if [
176            id,
177            &request.grant,
178            &act.target,
179            &act.holder,
180            &act.responsible,
181        ]
182        .iter()
183        .any(|value| value.is_empty() || value.len() > bound || value.chars().any(char::is_control))
184        {
185            return Err(SiteError::Invalid("held metadata"));
186        }
187        let verified = SiteAct::prepare(act.target.clone(), act.change.clone(), pass)?;
188        let exact = serde_json::to_string(&verified).map_err(|_| SiteError::Store)?;
189        if exact != request.words
190            || request.words.len() > bindings.limits.request_bytes.get()
191            || request.request_digest != lys_pass::drafts::request_digest(&exact)
192            || request.target.kind != act.change.kind()
193            || request.target.id != act.permission_id(self.network_id())
194            || request.target.action != act.change.action()
195        {
196            return Err(SiteError::Invalid("held immutable request"));
197        }
198        // Held, so no approver yet; the mode is the one its preparation recorded.
199        let mode = match mode {
200            crate::auth_lys::GrantMode::ByDraft => lys_pass::Mode::ByDraft,
201            crate::auth_lys::GrantMode::ByTwo => lys_pass::Mode::ByTwo,
202            crate::auth_lys::GrantMode::Outright => {
203                return Err(SiteError::Invalid("held approval mode"));
204            }
205        };
206        let draft = ApprovedDraft {
207            id: id.into(),
208            app: "liminal".into(),
209            grant: request.grant.clone(),
210            target: request.target.clone(),
211            request_digest: request.request_digest.clone(),
212            approved_by: Vec::new(),
213            mode,
214            words: exact,
215        };
216        let mut registry = self.lock()?;
217        if let Some(old) = registry.construction.drafts.get(id) {
218            return if same_draft(&old.draft, &draft) {
219                Ok(())
220            } else {
221                Err(SiteError::Invalid("held draft conflict"))
222            };
223        }
224        if registry.construction.drafts.len() >= bindings.limits.drafts.get() {
225            return Err(SiteError::Bound("drafts"));
226        }
227        let mut snapshot = Self::snapshot(&registry);
228        snapshot.construction.drafts.insert(
229            id.into(),
230            Prepared {
231                draft,
232                act: verified,
233                at_ms,
234            },
235        );
236        super::storage::commit(self, &mut registry, &snapshot)?;
237        drop(registry);
238        Ok(())
239    }
240}
241
242fn same_draft(left: &ApprovedDraft, right: &ApprovedDraft) -> bool {
243    left.id == right.id
244        && left.app == right.app
245        && left.grant == right.grant
246        && left.target.kind == right.target.kind
247        && left.target.id == right.target.id
248        && left.target.action == right.target.action
249        && left.request_digest == right.request_digest
250        && left.words == right.words
251}
252
253fn storage_error(_: SiteError) -> DraftError {
254    DraftError::Store
255}
256
257impl AtomicDraftStore for SiteDraftStore {
258    fn receipt(&self, draft: &str) -> Result<Option<Execution>, DraftError> {
259        Ok(self
260            .lifecycle
261            .lock()
262            .map_err(|_| DraftError::Store)?
263            .construction
264            .outcomes
265            .get(draft)
266            .map(|outcome| outcome.receipt.clone()))
267    }
268    fn approval(&self, draft: &ApprovedDraft) -> Result<ApprovalEvidence, DraftError> {
269        self.approvals.read(draft)
270    }
271    fn commit(
272        &mut self,
273        draft: &ApprovedDraft,
274        transition: &ApprovalTransition,
275    ) -> Result<Execution, DraftError> {
276        self.validate_approval(draft, transition)?;
277        let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
278        if let Some(outcome) = registry.construction.outcomes.get(&draft.id) {
279            let prepared = registry
280                .construction
281                .drafts
282                .get(&draft.id)
283                .ok_or(DraftError::Contract)?;
284            if !same_draft(&prepared.draft, draft) {
285                return Err(DraftError::ReceiptMismatch);
286            }
287            return Ok(outcome.receipt.clone());
288        }
289        let prepared = registry
290            .construction
291            .drafts
292            .get(&draft.id)
293            .cloned()
294            .ok_or(DraftError::Contract)?;
295        if !same_draft(&prepared.draft, draft)
296            || transition.draft != draft.id
297            || transition.digest != draft.request_digest
298            || transition.approved_by.is_empty()
299        {
300            return Err(DraftError::ReceiptMismatch);
301        }
302        let mut snapshot = Lifecycle::snapshot(&registry);
303        let applied = self.lifecycle.apply_site_act(
304            &mut snapshot,
305            &prepared.act,
306            transition,
307            prepared.at_ms,
308            &registry.trials,
309        );
310        let receipt = match applied {
311            Ok(()) => {
312                let bytes = serde_json::to_string(&(
313                    transition,
314                    &snapshot.records,
315                    &snapshot.construction.templates,
316                ))
317                .map_err(|_| DraftError::Store)?;
318                Execution::Executed {
319                    request_digest: draft.request_digest.clone(),
320                    receipt_digest: lys_pass::drafts::request_digest(&bytes),
321                }
322            }
323            Err(SiteError::Pass(name)) => return Err(DraftError::Lys(name)),
324            Err(SiteError::Store | SiteError::Lifecycle(_)) => {
325                return Err(DraftError::Store);
326            }
327            Err(error) => {
328                snapshot = Lifecycle::snapshot(&registry);
329                Execution::RefusedOnExecution {
330                    request_digest: draft.request_digest.clone(),
331                    refusal: "site_execution_refused".into(),
332                    reason: error.to_string(),
333                }
334            }
335        };
336        if matches!(receipt, Execution::Executed { .. }) {
337            for record in snapshot.records.values_mut() {
338                if let Some(approval) = record
339                    .approvals
340                    .last_mut()
341                    .filter(|approval| approval.draft == draft.id)
342                {
343                    approval.receipt = Some(receipt.clone());
344                }
345            }
346            for template in snapshot.construction.templates.values_mut() {
347                if let Some(approval) = template
348                    .transitions
349                    .last_mut()
350                    .filter(|approval| approval.draft == draft.id)
351                {
352                    approval.receipt = Some(receipt.clone());
353                }
354            }
355        }
356        snapshot.construction.outcomes.insert(
357            draft.id.clone(),
358            Outcome {
359                receipt: receipt.clone(),
360                approval: transition.clone(),
361                acknowledged: false,
362                delivered: false,
363            },
364        );
365        let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
366            .map_err(|_| DraftError::Store);
367        drop(registry);
368        result?;
369        self.lifecycle.refresh_sites().map_err(storage_error)?;
370        Ok(receipt)
371    }
372    fn acknowledged(&self, draft: &str) -> Result<bool, DraftError> {
373        Ok(self
374            .lifecycle
375            .lock()
376            .map_err(|_| DraftError::Store)?
377            .construction
378            .outcomes
379            .get(draft)
380            .is_some_and(|outcome| outcome.acknowledged))
381    }
382    fn acknowledge(&mut self, draft: &str, receipt: &Execution) -> Result<(), DraftError> {
383        let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
384        let mut snapshot = Lifecycle::snapshot(&registry);
385        let outcome = snapshot
386            .construction
387            .outcomes
388            .get_mut(draft)
389            .ok_or(DraftError::ReceiptMismatch)?;
390        if &outcome.receipt != receipt {
391            return Err(DraftError::ReceiptMismatch);
392        }
393        if outcome.acknowledged {
394            return Ok(());
395        }
396        outcome.acknowledged = true;
397        let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
398            .map_err(|_| DraftError::Store);
399        drop(registry);
400        result
401    }
402    fn pending_acknowledgments(
403        &self,
404        limit: NonZeroUsize,
405    ) -> Result<Vec<ApprovedDraft>, DraftError> {
406        let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
407        registry
408            .construction
409            .outcomes
410            .iter()
411            .filter(|(_, outcome)| !outcome.acknowledged)
412            .take(limit.get())
413            .map(|(id, _)| {
414                registry
415                    .construction
416                    .drafts
417                    .get(id)
418                    .map(|prepared| prepared.draft.clone())
419                    .ok_or(DraftError::Store)
420            })
421            .collect()
422    }
423    fn deliver_outcome(
424        &mut self,
425        draft: &ApprovedDraft,
426        receipt: &Execution,
427    ) -> Result<(), DraftError> {
428        let (holder, delivered) = {
429            let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
430            let outcome = registry
431                .construction
432                .outcomes
433                .get(&draft.id)
434                .ok_or(DraftError::ReceiptMismatch)?;
435            let prepared = registry
436                .construction
437                .drafts
438                .get(&draft.id)
439                .ok_or(DraftError::ReceiptMismatch)?;
440            if &outcome.receipt != receipt || !same_draft(&prepared.draft, draft) {
441                return Err(DraftError::ReceiptMismatch);
442            }
443            let result = (prepared.act.holder.clone(), outcome.delivered);
444            drop(registry);
445            result
446        };
447        if delivered {
448            return Ok(());
449        }
450        self.delivery.deliver(&holder, draft, receipt)?;
451        let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
452        let mut snapshot = Lifecycle::snapshot(&registry);
453        snapshot
454            .construction
455            .outcomes
456            .get_mut(&draft.id)
457            .ok_or(DraftError::Store)?
458            .delivered = true;
459        let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
460            .map_err(|_| DraftError::Store);
461        drop(registry);
462        result
463    }
464}
465
466#[path = "executor_run.rs"]
467mod run;