Skip to main content

rustev_runtime/
live.rs

1//! Live re-execution with fresh observations (spec 026).
2//!
3//! [`Runtime::reexecute_live`] is the only entry point that may request
4//! fresh external work for a previously recorded decision shape, and the
5//! only one that accepts a host [`LiveAuthorizer`]. It runs a new decision:
6//! a decision id unequal to the historical one, a snapshot whose identity the
7//! runtime computes, a fresh evaluation time, fresh admission, deadline,
8//! concurrency, retry, fallback and budget state, and new observations.
9//! Nothing is read from a replay bundle; a historical case is a link (a
10//! digest plus identities) carried into the evidence for later comparison.
11//!
12//! Immediately before every external attempt, retries and fallbacks
13//! included, and after its full worst-case exposure is reserved, the host is
14//! asked about that exact attempt. A missing, expired, mismatched, errored
15//! or refused answer prevents dispatch and is recorded as
16//! `authorization_refused`; the runtime never downgrades scope, switches
17//! targets, retries or falls back to avoid a refusal. The answer is scoped
18//! to that attempt, is never exported, and authorizes nothing downstream.
19
20use std::collections::BTreeMap;
21use std::fmt;
22use std::panic::{AssertUnwindSafe, catch_unwind};
23use std::sync::{Arc, Mutex};
24
25use rustev_contract::definition::{Operation, StepBody};
26use rustev_contract::ids::{ContentDigest, SnapshotId};
27use rustev_contract::live::{
28    Admission, AttemptRole, AuthorizationOutcome, AuthorizationReference, AuthorizationRefusal,
29    AuthorizationRequest, CapturePosture, ComparisonSection, DeliveryObservation, Dispatch,
30    Exposure, Historical, HistoricalLink, HistoricalRetention, LiveEffect, LiveReexecution,
31    MAX_EFFECTS, RetentionPosture, RunLink,
32};
33use rustev_contract::plan::{PlanExecution, PlanStepDetail};
34use rustev_contract::run::{Charge, CoreEvidence, CostBound, RunRecord, Termination};
35use rustev_contract::scope::{Handle, Scope};
36use rustev_contract::snapshot::Snapshot;
37use rustev_contract::time::Timestamp;
38use rustev_contract::{Document, Identified, schema};
39use rustev_core::evaluate::SemanticRequest;
40use rustev_core::seams::CancelSignal;
41
42use crate::driver::{DETAIL_BYTES, DecisionCtx};
43use crate::wait::cut;
44use crate::{
45    CaptureConfig, CaptureConfigError, CaptureOutcome, Completion, DecideError, Decided,
46    DecisionRequest, Delivery, DeliveryFailure, PreparedPlan, Rejection, Runtime, Target, capture,
47};
48
49/// The host's answer about one proposed attempt. In process only: it is
50/// neither serializable nor clonable, the runtime keeps nothing of it but
51/// the reference, and it authorizes no action outside that one dispatch.
52#[derive(Debug)]
53pub enum DispatchAnswer {
54    /// Send exactly the attempt whose request digest is `answers`, if it is
55    /// sent within `valid_for_ms` of the question, by the runtime's clock.
56    Allow {
57        answers: ContentDigest,
58        reference: AuthorizationReference,
59        valid_for_ms: u64,
60    },
61    Refuse {
62        reference: Option<AuthorizationReference>,
63        detail: String,
64    },
65}
66
67/// The host's authorization seam (spec 026, 3.3). Rustev mints no token:
68/// the host decides, from its own trusted facts, whether this exact attempt
69/// may be sent. `Ok(None)` is no answer (missing); `Err` is a failure of the
70/// authorizer (errored); a panic is contained and errored. Every one of
71/// them prevents dispatch.
72pub trait LiveAuthorizer: Send + Sync {
73    fn authorize(&self, request: &AuthorizationRequest) -> Result<Option<DispatchAnswer>, String>;
74
75    /// Source dispatch is independently authorized. Existing hosts refuse it.
76    fn authorize_acquisition(
77        &self,
78        _request: &AcquisitionAuthorizationRequest,
79    ) -> Result<Option<DispatchAnswer>, String> {
80        Ok(None)
81    }
82}
83
84/// The exact source attempt presented to the host, with its own digest domain.
85#[derive(Debug, Clone)]
86pub struct AcquisitionAuthorizationRequest {
87    pub attempt: crate::AcquisitionSpendAttempt,
88    pub subject: Handle,
89    pub scope: Scope,
90    pub step: String,
91    pub tier: u32,
92    pub page: u32,
93    pub reserved_units: u64,
94    pub remaining_ms: u64,
95}
96impl AcquisitionAuthorizationRequest {
97    pub fn digest(&self) -> Result<ContentDigest, rustev_contract::canonical::CanonicalError> {
98        use rustev_contract::canonical::{canonical_bytes, tagged_digest};
99        let a = &self.attempt;
100        let bytes = canonical_bytes(&serde_json::json!({
101            "decision_id": a.decision_id, "attempt_id": a.attempt_id,
102            "source": a.source, "operation": a.operation, "request": a.request,
103            "bound": a.bound, "subject": self.subject, "scope": self.scope,
104            "step": self.step, "tier": self.tier, "page": self.page,
105            "reserved_units": self.reserved_units, "remaining_ms": self.remaining_ms,
106        }))?;
107        Ok(ContentDigest::parse(&tagged_digest(
108            "rustev.live-acquisition-authorization/1",
109            &bytes,
110        ))
111        .expect("computed digest"))
112    }
113}
114
115/// Source observations kept separate from the published live result.
116#[derive(Debug, Clone, Default)]
117pub struct SourceOutcome {
118    pub incomparable: Option<String>,
119    pub captures: BTreeMap<String, Vec<ContentDigest>>,
120    pub refusals: Vec<(String, String)>,
121}
122
123/// One live re-execution to run. Nothing here is taken from a replay bundle.
124#[derive(Debug, Clone)]
125pub struct LiveRequest {
126    /// A new decision id: never the historical one, and not running.
127    pub decision_id: String,
128    /// The current snapshot, validated by the normal snapshot contract.
129    pub snapshot: Snapshot,
130    /// The identity the caller believes the snapshot has, if it states one.
131    /// Never trusted: the runtime computes the identity, and a different
132    /// stated identity is refused.
133    pub expected_snapshot_id: Option<SnapshotId>,
134    /// Supplied anew; never read from a clock or a historical record.
135    pub evaluation_time: Timestamp,
136    /// Can only tighten the plan's `deadline_ms`.
137    pub deadline_ms: Option<u64>,
138    /// Passed through to backends, as for `decide`.
139    pub principal_handle: Vec<u8>,
140    /// Opaque, non-secret subject and scope presented to the authorizer.
141    pub subject: Handle,
142    pub scope: Scope,
143    /// The new run's own capture; off unless the host chooses it.
144    pub capture: Option<CaptureConfig>,
145    /// The historical case this run may later be compared with, by link.
146    pub historical: Option<HistoricalLink>,
147}
148
149/// Why a live re-execution was refused or failed. Except for
150/// `GenerationFailed`, nothing was presented to the authorizer, dispatched
151/// or spent.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum LiveRefusal {
154    /// A declared source has no unique posture in the gated request.
155    SpendAcquisitionUnsupported,
156    /// The decision id is the historical decision's.
157    HistoricalDecisionId,
158    /// The historical link names a decision id outside 1 to 256 bytes.
159    InvalidHistoricalLink,
160    /// The snapshot's computed identity is not the identity stated for it.
161    SnapshotIdentity {
162        stated: SnapshotId,
163        computed: SnapshotId,
164    },
165    /// The snapshot has no canonical identity.
166    InvalidSnapshot(String),
167    Capture(CaptureConfigError),
168    /// The plan can propose more attempts than one live document records.
169    Unbounded {
170        worst_case: u64,
171        max: u64,
172    },
173    /// The runtime's own admission refusal (spec 003, 3.4.1).
174    Rejected(Rejection),
175    /// A pure generation invariant failed: no text result and no record.
176    /// This is a runtime failure, not a request defect.
177    GenerationFailed(String),
178}
179
180impl fmt::Display for LiveRefusal {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            LiveRefusal::SpendAcquisitionUnsupported => {
184                f.write_str("gated live acquisition dispatch is not implemented")
185            }
186            LiveRefusal::HistoricalDecisionId => {
187                f.write_str("a live re-execution needs a new decision id, not the historical one")
188            }
189            LiveRefusal::InvalidHistoricalLink => {
190                f.write_str("the historical link's decision id is not 1 to 256 bytes")
191            }
192            LiveRefusal::SnapshotIdentity { stated, computed } => write!(
193                f,
194                "identity mismatch: the snapshot is {computed}, not the stated {stated}"
195            ),
196            LiveRefusal::InvalidSnapshot(e) => write!(f, "the snapshot has no identity: {e}"),
197            LiveRefusal::Capture(e) => write!(f, "{e}"),
198            LiveRefusal::Unbounded { worst_case, max } => write!(
199                f,
200                "the plan can propose {worst_case} attempts; a live document records at most {max}"
201            ),
202            LiveRefusal::Rejected(r) => write!(f, "not admitted: {r:?}"),
203            LiveRefusal::GenerationFailed(detail) => write!(f, "generation failed: {detail}"),
204        }
205    }
206}
207
208impl std::error::Error for LiveRefusal {}
209
210/// A finished live re-execution: the ordinary result of a new decision and
211/// its live document, returned also when evidence delivery failed, so every
212/// attempt, charge and remote state stays with the caller (spec 026, 5).
213#[derive(Debug)]
214pub struct LiveResult {
215    /// First spend refusal; later attempts of this decision were not sent.
216    pub spend_refusal: Option<crate::SpendRefusal>,
217    pub completion: Completion,
218    /// The new run record; the evidence of execution.
219    pub record: Arc<RunRecord>,
220    pub delivery: Result<Delivery, DeliveryFailure>,
221    /// `None` when capture was off.
222    pub capture: Option<CaptureOutcome>,
223    pub document: LiveReexecution,
224}
225
226/// One proposed attempt as presented, and the host's answer.
227struct Presented {
228    step: String,
229    instance: Vec<String>,
230    target: u32,
231    role: AttemptRole,
232    attempt_id: String,
233    backend_id: String,
234    artifact: rustev_contract::ids::ArtifactId,
235    request: rustev_contract::ids::RequestId,
236    presented: ContentDigest,
237    exposure: Exposure,
238    remaining_ms: u64,
239    authorization: AuthorizationOutcome,
240}
241
242/// The live path's state within one decision.
243pub(crate) struct LiveCtx<'l> {
244    authorizer: &'l dyn LiveAuthorizer,
245    spend: Option<crate::LiveSpend<'l>>,
246    sources: crate::LiveSources<'l>,
247    spend_refusal: Mutex<Option<crate::SpendRefusal>>,
248    acquisition_incomparable: Mutex<Option<String>>,
249    acquisition_captures: Mutex<BTreeMap<String, Vec<ContentDigest>>>,
250    acquisition_refusals: Mutex<Vec<(String, String)>>,
251    subject: &'l Handle,
252    scope: &'l Scope,
253    operations: BTreeMap<String, Operation>,
254    presented: Mutex<Vec<Presented>>,
255}
256
257/// What [`LiveCtx::authorize`] needs to know about the proposed attempt.
258pub(crate) struct Proposal<'p> {
259    pub req: &'p SemanticRequest,
260    pub target_index: usize,
261    pub target: &'p Target,
262    pub role: AttemptRole,
263    pub attempt_id: &'p str,
264    pub bound: CostBound,
265    pub reserved: u64,
266}
267
268impl LiveCtx<'_> {
269    pub(crate) fn captured_page(
270        &self,
271        r: &rustev_core::evaluate::PendingAcquisition,
272    ) -> Option<Result<Vec<u8>, ()>> {
273        self.spend?;
274        let posture = self
275            .sources
276            .acquisitions
277            .iter()
278            .find(|p| p.source == r.request.source)?;
279        if posture.posture != crate::AcquisitionPosture::Captured {
280            return None;
281        }
282        let id = r.request.id().ok();
283        // A page that cannot be canonicalized is not evidence; among the
284        // rest, one request identity with different bytes is ambiguous.
285        let mut pages = self
286            .sources
287            .pages
288            .iter()
289            .filter(|p| Some(&p.request) == id.as_ref())
290            .filter_map(|p| p.record_canonical().ok());
291        let first = pages.next();
292        let ambiguous = first.as_ref().is_some_and(|b| pages.any(|p| &p != b));
293        if let Some(bytes) = first.filter(|_| !ambiguous) {
294            let digest = rustev_contract::acquisition::AcquisitionPage::parse(&bytes)
295                .expect("canonical page")
296                .record_digest()
297                .expect("canonical page digest");
298            let mut captures = self
299                .acquisition_captures
300                .lock()
301                .unwrap_or_else(|e| e.into_inner());
302            let digests = captures.entry(r.request.source.to_string()).or_default();
303            if !digests.contains(&digest) {
304                digests.push(digest);
305                digests.sort();
306            }
307            Some(Ok(bytes))
308        } else {
309            let mut reason = self
310                .acquisition_incomparable
311                .lock()
312                .unwrap_or_else(|e| e.into_inner());
313            if reason.is_none() || ambiguous {
314                *reason = Some(
315                    if ambiguous {
316                        "ambiguous-output"
317                    } else {
318                        "acquisition-not-captured"
319                    }
320                    .into(),
321                );
322            }
323            Some(Err(()))
324        }
325    }
326
327    pub(crate) fn source_spend(
328        &self,
329        cx: &DecisionCtx<'_, '_>,
330        r: &rustev_core::evaluate::PendingAcquisition,
331        attempt_id: &str,
332        bound: CostBound,
333        reserved: u64,
334    ) -> Result<Option<crate::spend::HeldSpend<'_>>, ()> {
335        let Some(spend) = self.spend else {
336            return Ok(None);
337        };
338        let refuse = |code: &str| {
339            self.acquisition_refusals
340                .lock()
341                .unwrap_or_else(|e| e.into_inner())
342                .push((attempt_id.into(), code.into()));
343        };
344        if let Some(r) = self.spend_refusal() {
345            refuse(&r.code);
346            return Err(());
347        }
348        let attempt = crate::AcquisitionSpendAttempt {
349            decision_id: cx.decision_id.into(),
350            attempt_id: attempt_id.into(),
351            source: r.request.source.clone(),
352            operation: r.operation.clone(),
353            request: r.request.id().map_err(|_| ())?,
354            bound,
355        };
356        let now = cx.rt.clock.now();
357        let asked = AcquisitionAuthorizationRequest {
358            attempt,
359            subject: self.subject.clone(),
360            scope: self.scope.clone(),
361            step: r.step.clone(),
362            tier: r.tier_index,
363            page: r.page_index,
364            reserved_units: reserved,
365            remaining_ms: cx.deadline.saturating_sub(now),
366        };
367        let digest = asked.digest().map_err(|_| ())?;
368        let until = match catch_unwind(AssertUnwindSafe(|| {
369            self.authorizer.authorize_acquisition(&asked)
370        })) {
371            Ok(Ok(Some(DispatchAnswer::Allow {
372                answers,
373                valid_for_ms,
374                ..
375            }))) if answers == digest && cx.rt.clock.now() < now.saturating_add(valid_for_ms) => {
376                now.saturating_add(valid_for_ms)
377            }
378            _ => {
379                refuse("authorization_refused");
380                return Err(());
381            }
382        };
383        let mut refusal = self.spend_refusal.lock().unwrap_or_else(|e| e.into_inner());
384        if let Some(r) = &*refusal {
385            refuse(&r.code);
386            return Err(());
387        }
388        let result = if bound == CostBound::Unknown {
389            Err(crate::SpendRefusal::new("spend-exposure-unknown"))
390        } else {
391            spend.gate.reserve_acquisition(&asked.attempt)
392        };
393        match result {
394            Ok(ticket) => {
395                let held = crate::spend::HeldSpend::new(spend.gate, ticket);
396                if cx.rt.clock.now() >= until {
397                    held.release();
398                    refuse("authorization_refused");
399                    Err(())
400                } else {
401                    Ok(Some(held))
402                }
403            }
404            Err(r) => {
405                refuse(&r.code);
406                *refusal = Some(r);
407                Err(())
408            }
409        }
410    }
411
412    pub(crate) fn spend_refusal(&self) -> Option<crate::SpendRefusal> {
413        self.spend_refusal
414            .lock()
415            .unwrap_or_else(|e| e.into_inner())
416            .clone()
417    }
418
419    pub(crate) fn reserve_spend(
420        &self,
421        cx: &DecisionCtx<'_, '_>,
422        p: Proposal<'_>,
423    ) -> Result<Option<crate::spend::HeldSpend<'_>>, crate::SpendRefusal> {
424        let Some(spend) = self.spend else {
425            return Ok(None);
426        };
427        let mut refusal = self.spend_refusal.lock().unwrap_or_else(|e| e.into_inner());
428        if let Some(r) = &*refusal {
429            return Err(r.clone());
430        }
431        let binding = spend
432            .bindings
433            .iter()
434            .find(|b| b.backend_id == p.target.backend_id && b.artifact == p.target.artifact);
435        let result = match binding {
436            None => Err(crate::SpendRefusal::new("spend-unit")),
437            Some(binding) => spend.gate.reserve(&crate::SpendAttempt {
438                decision_id: cx.decision_id.to_string(),
439                attempt_id: p.attempt_id.to_string(),
440                backend_id: p.target.backend_id.clone(),
441                target: p.target_index as u32,
442                artifact: p.target.artifact.clone(),
443                operation: self.operations[&p.req.step],
444                bound: p.bound,
445                cost_unit: binding.cost_unit,
446            }),
447        };
448        match result {
449            Ok(ticket) => Ok(Some(crate::spend::HeldSpend::new(spend.gate, ticket))),
450            Err(r) => {
451                *refusal = Some(r.clone());
452                Err(r)
453            }
454        }
455    }
456
457    pub(crate) fn expire_authorization(&self, attempt_id: &str) {
458        let mut presented = self.presented.lock().unwrap_or_else(|e| e.into_inner());
459        if let Some(p) = presented.iter_mut().find(|p| p.attempt_id == attempt_id)
460            && let AuthorizationOutcome::Allowed { reference } = &p.authorization
461        {
462            p.authorization = AuthorizationOutcome::Refused {
463                refusal: AuthorizationRefusal::Expired,
464                reference: Some(reference.clone()),
465                detail: "authorization expired during spend reservation".into(),
466            };
467        }
468    }
469
470    /// Present one proposed attempt to the host, after its reservation and
471    /// immediately before dispatch, and record the answer. `Ok` only for an
472    /// allow that names this exact request and is still valid now.
473    pub(crate) fn authorize(
474        &self,
475        cx: &DecisionCtx<'_, '_>,
476        p: Proposal<'_>,
477    ) -> Result<u64, AuthorizationRefusal> {
478        let now = cx.rt.clock.now();
479        let identity = cx
480            .ev()
481            .request_document(
482                &p.req.step,
483                &p.req.instance,
484                p.target_index as u32,
485                self.scope,
486            )
487            .map_err(|e| e.to_string())
488            .and_then(|d| d.id().map_err(|e| e.to_string()));
489        let request = match identity {
490            Ok(id) => id,
491            // The request is pending and its target bound, so this is a
492            // defect; it still prevents dispatch rather than guessing.
493            Err(_) => return Err(AuthorizationRefusal::Errored),
494        };
495        let asked = AuthorizationRequest {
496            decision_id: cx.decision_id.to_string(),
497            subject: self.subject.clone(),
498            scope: self.scope.clone(),
499            step: p.req.step.clone(),
500            instance: p.req.instance.clone(),
501            operation: self.operations[&p.req.step],
502            target: p.target_index as u32,
503            role: p.role,
504            backend_id: p.target.backend_id.clone(),
505            artifact: p.target.artifact.clone(),
506            request: request.clone(),
507            attempt_id: p.attempt_id.to_string(),
508            exposure: Exposure {
509                bound: p.bound,
510                reserved_units: p.reserved,
511            },
512            remaining_ms: cx.deadline.saturating_sub(now),
513        };
514        let Ok(digest) = asked.digest() else {
515            return Err(AuthorizationRefusal::Errored);
516        };
517        let refused = |refusal, reference, detail: &str| AuthorizationOutcome::Refused {
518            refusal,
519            reference,
520            detail: cut(detail, DETAIL_BYTES),
521        };
522        let mut valid_until = now;
523        let outcome = match catch_unwind(AssertUnwindSafe(|| self.authorizer.authorize(&asked))) {
524            Err(p) => refused(
525                AuthorizationRefusal::Errored,
526                None,
527                &format!(
528                    "authorizer panicked: {}",
529                    crate::wait::panic_detail(p.as_ref())
530                ),
531            ),
532            Ok(Err(detail)) => refused(AuthorizationRefusal::Errored, None, &detail),
533            Ok(Ok(None)) => refused(AuthorizationRefusal::Missing, None, "no answer"),
534            Ok(Ok(Some(DispatchAnswer::Refuse { reference, detail }))) => {
535                refused(AuthorizationRefusal::Refused, reference, &detail)
536            }
537            Ok(Ok(Some(DispatchAnswer::Allow {
538                answers,
539                reference,
540                valid_for_ms,
541            }))) => {
542                if answers != digest {
543                    refused(
544                        AuthorizationRefusal::Mismatched,
545                        Some(reference),
546                        "the answer names another request",
547                    )
548                } else if cx.rt.clock.now() >= now.saturating_add(valid_for_ms) {
549                    refused(
550                        AuthorizationRefusal::Expired,
551                        Some(reference),
552                        &format!("the answer was valid for {valid_for_ms} ms"),
553                    )
554                } else {
555                    valid_until = now.saturating_add(valid_for_ms);
556                    AuthorizationOutcome::Allowed { reference }
557                }
558            }
559        };
560        let result = match &outcome {
561            AuthorizationOutcome::Allowed { .. } => Ok(valid_until),
562            AuthorizationOutcome::Refused { refusal, .. } => Err(*refusal),
563        };
564        self.presented
565            .lock()
566            .unwrap_or_else(|e| e.into_inner())
567            .push(Presented {
568                step: asked.step,
569                instance: asked.instance,
570                target: asked.target,
571                role: asked.role,
572                attempt_id: asked.attempt_id,
573                backend_id: asked.backend_id,
574                artifact: asked.artifact,
575                request,
576                presented: digest,
577                exposure: asked.exposure,
578                remaining_ms: asked.remaining_ms,
579                authorization: outcome,
580            });
581        result
582    }
583}
584
585/// The worst-case number of attempts a plan can propose: its execution
586/// policy's bound, or one per possible request without a policy. A refused
587/// proposal ends its request, so it replaces an attempt, never adds one.
588fn worst_case_attempts(plan: &PreparedPlan) -> u64 {
589    match &plan.compiled().plan.execution {
590        PlanExecution::Declared(d) => d.policy.max_attempts_per_decision,
591        PlanExecution::None => plan
592            .compiled()
593            .plan
594            .steps
595            .iter()
596            .filter_map(|s| match &s.detail {
597                PlanStepDetail::Semantic(b) => Some(b.max_requests),
598                PlanStepDetail::Rank(b) => Some(b.max_requests),
599                _ => None,
600            })
601            .fold(0, u64::saturating_add),
602    }
603}
604
605impl Runtime {
606    /// Re-execute a decision shape live (spec 026): a new decision, under
607    /// fresh bounds, with the host authorizing every external attempt
608    /// immediately before it is sent. The only entry point that accepts an
609    /// authorizer; `decide`, capture, replay and comparison never dispatch
610    /// on its behalf.
611    ///
612    /// Refused before admission, with nothing presented or spent: a reused
613    /// historical decision id, a snapshot whose computed identity is not the
614    /// stated one, an invalid capture configuration, a plan whose proposals
615    /// exceed the document bound, and every admission refusal of `decide`.
616    ///
617    /// ```no_run
618    /// # async fn f(rt: &rustev_runtime::Runtime, plan: &rustev_runtime::PreparedPlan,
619    /// #     live: rustev_runtime::LiveRequest, a: &dyn rustev_runtime::LiveAuthorizer,
620    /// #     req: rustev_runtime::DecisionRequest) {
621    /// let cancel = rustev_core::seams::CancelSignal::new();
622    /// let _ = rt.reexecute_live(plan, live, a, &cancel).await;
623    /// let _ = rt.decide(plan, req, &cancel).await;
624    /// # }
625    /// ```
626    ///
627    /// ```compile_fail
628    /// # async fn f(rt: &rustev_runtime::Runtime, plan: &rustev_runtime::PreparedPlan,
629    /// #     req: rustev_runtime::DecisionRequest, a: &dyn rustev_runtime::LiveAuthorizer) {
630    /// // `decide` takes no authorizer: only `reexecute_live` does.
631    /// rt.decide(plan, req, &rustev_core::seams::CancelSignal::new(), a).await;
632    /// # }
633    /// ```
634    ///
635    /// ```compile_fail
636    /// # async fn f(rt: &rustev_runtime::Runtime, plan: &rustev_runtime::PreparedPlan,
637    /// #     req: rustev_runtime::DecisionRequest, c: &rustev_runtime::CaptureConfig,
638    /// #     a: &dyn rustev_runtime::LiveAuthorizer) {
639    /// // Nor does capture.
640    /// rt.decide_with_capture(plan, req, &rustev_core::seams::CancelSignal::new(), c, a).await;
641    /// # }
642    /// ```
643    pub async fn reexecute_live(
644        &self,
645        plan: &PreparedPlan,
646        req: LiveRequest,
647        authorizer: &dyn LiveAuthorizer,
648        cancel: &CancelSignal,
649    ) -> Result<LiveResult, LiveRefusal> {
650        self.reexecute_live_inner(
651            plan,
652            req,
653            authorizer,
654            cancel,
655            None,
656            crate::LiveSources::default(),
657        )
658        .await
659        .map(|(result, _)| result)
660    }
661
662    /// Live re-execution with a durable spend gate, after budget and host
663    /// authorization on every attempt. Existing ungated callers are unchanged.
664    /// Plans with acquisitions are refused before admission.
665    /// Use `reexecute_live_with_sources` to supply source postures.
666    pub async fn reexecute_live_with_gate(
667        &self,
668        plan: &PreparedPlan,
669        req: LiveRequest,
670        authorizer: &dyn LiveAuthorizer,
671        cancel: &CancelSignal,
672        spend: crate::LiveSpend<'_>,
673    ) -> Result<LiveResult, LiveRefusal> {
674        if plan
675            .compiled()
676            .plan
677            .acquisitions
678            .as_ref()
679            .is_some_and(|a| !a.is_empty())
680        {
681            return Err(LiveRefusal::SpendAcquisitionUnsupported);
682        }
683        self.reexecute_live_with_sources(
684            plan,
685            req,
686            authorizer,
687            cancel,
688            spend,
689            crate::LiveSources::default(),
690        )
691        .await
692        .map(|(result, _)| result)
693    }
694
695    /// Gated live execution with explicit source postures and separate observations.
696    pub async fn reexecute_live_with_sources(
697        &self,
698        plan: &PreparedPlan,
699        req: LiveRequest,
700        authorizer: &dyn LiveAuthorizer,
701        cancel: &CancelSignal,
702        spend: crate::LiveSpend<'_>,
703        sources: crate::LiveSources<'_>,
704    ) -> Result<(LiveResult, SourceOutcome), LiveRefusal> {
705        self.reexecute_live_inner(plan, req, authorizer, cancel, Some(spend), sources)
706            .await
707    }
708
709    async fn reexecute_live_inner(
710        &self,
711        plan: &PreparedPlan,
712        req: LiveRequest,
713        authorizer: &dyn LiveAuthorizer,
714        cancel: &CancelSignal,
715        spend: Option<crate::LiveSpend<'_>>,
716        sources: crate::LiveSources<'_>,
717    ) -> Result<(LiveResult, SourceOutcome), LiveRefusal> {
718        if spend.is_some_and(|_| {
719            plan.compiled()
720                .plan
721                .acquisitions
722                .iter()
723                .flatten()
724                .flat_map(|a| &a.sources)
725                .any(|a| {
726                    sources
727                        .acquisitions
728                        .iter()
729                        .filter(|p| p.source == a.source.id)
730                        .count()
731                        != 1
732                })
733        }) {
734            return Err(LiveRefusal::SpendAcquisitionUnsupported);
735        }
736        if req
737            .historical
738            .as_ref()
739            .is_some_and(|h| h.decision_id == req.decision_id)
740        {
741            return Err(LiveRefusal::HistoricalDecisionId);
742        }
743        if req
744            .historical
745            .as_ref()
746            .is_some_and(|h| h.decision_id.is_empty() || h.decision_id.len() > 256)
747        {
748            return Err(LiveRefusal::InvalidHistoricalLink);
749        }
750        let snapshot_id = req
751            .snapshot
752            .id()
753            .map_err(|e| LiveRefusal::InvalidSnapshot(e.to_string()))?;
754        if let Some(stated) = &req.expected_snapshot_id
755            && *stated != snapshot_id
756        {
757            return Err(LiveRefusal::SnapshotIdentity {
758                stated: stated.clone(),
759                computed: snapshot_id,
760            });
761        }
762        if let Some(c) = &req.capture {
763            c.check().map_err(LiveRefusal::Capture)?;
764        }
765        let worst_case = worst_case_attempts(plan);
766        if worst_case > MAX_EFFECTS as u64 {
767            return Err(LiveRefusal::Unbounded {
768                worst_case,
769                max: MAX_EFFECTS as u64,
770            });
771        }
772
773        let operations = plan
774            .compiled()
775            .plan
776            .definition
777            .steps
778            .iter()
779            .filter_map(|s| match &s.body {
780                StepBody::Semantic(d) => Some((s.id.clone(), d.operation)),
781                StepBody::Rank(_) => {
782                    Some((s.id.clone(), rustev_contract::definition::Operation::Rank))
783                }
784                _ => None,
785            })
786            .collect();
787        let live = LiveCtx {
788            authorizer,
789            spend,
790            sources,
791            spend_refusal: Mutex::new(None),
792            acquisition_incomparable: Mutex::new(None),
793            acquisition_captures: Mutex::new(BTreeMap::new()),
794            acquisition_refusals: Mutex::new(vec![]),
795            subject: &req.subject,
796            scope: &req.scope,
797            operations,
798            presented: Mutex::new(vec![]),
799        };
800        let decision = DecisionRequest {
801            decision_id: req.decision_id.clone(),
802            snapshot: req.snapshot.clone(),
803            evaluation_time: req.evaluation_time,
804            deadline_ms: req.deadline_ms,
805            principal_handle: req.principal_handle.clone(),
806        };
807        let mut capturing = req.capture.as_ref().map(capture::Capturing::new);
808        let result = self
809            .run(
810                plan,
811                decision,
812                cancel,
813                capturing.as_mut(),
814                Some(&live),
815                None,
816                None,
817                &BTreeMap::new(),
818            )
819            .await;
820        let (record, delivery) = match result {
821            Err(DecideError::GenerationFailed(detail)) => {
822                return Err(LiveRefusal::GenerationFailed(detail));
823            }
824            Err(DecideError::Rejected(r)) => return Err(LiveRefusal::Rejected(r)),
825            Ok(Decided {
826                record, delivery, ..
827            }) => (record, Ok(delivery)),
828            Err(DecideError::EvidenceNotDelivered { record, failure }) => (record, Err(failure)),
829        };
830        let capture = capturing.map(|c| {
831            CaptureOutcome::Captured(c.finish(
832                &req.decision_id,
833                record.termination == Termination::Cancelled,
834            ))
835        });
836        let spend_refusal = live.spend_refusal();
837        let presented = live
838            .presented
839            .into_inner()
840            .unwrap_or_else(|e| e.into_inner());
841        let document = document(&req, snapshot_id, &record, &delivery, &capture, presented);
842        let completion = match &record.core {
843            CoreEvidence::Recorded(e) if record.termination == Termination::Judged => {
844                Completion::Judged(Box::new(e.judgment.clone()))
845            }
846            _ => Completion::Cancelled,
847        };
848        let sources = SourceOutcome {
849            incomparable: live
850                .acquisition_incomparable
851                .into_inner()
852                .unwrap_or_else(|e| e.into_inner()),
853            captures: live
854                .acquisition_captures
855                .into_inner()
856                .unwrap_or_else(|e| e.into_inner()),
857            refusals: live
858                .acquisition_refusals
859                .into_inner()
860                .unwrap_or_else(|e| e.into_inner()),
861        };
862        Ok((
863            LiveResult {
864                spend_refusal,
865                completion,
866                record,
867                delivery,
868                capture,
869                document,
870            },
871            sources,
872        ))
873    }
874}
875
876fn document(
877    req: &LiveRequest,
878    snapshot_id: SnapshotId,
879    record: &RunRecord,
880    delivery: &Result<Delivery, DeliveryFailure>,
881    capture: &Option<CaptureOutcome>,
882    presented: Vec<Presented>,
883) -> LiveReexecution {
884    let attempts: BTreeMap<&str, &rustev_contract::run::AttemptRecord> = record
885        .requests
886        .iter()
887        .flat_map(|r| &r.attempts)
888        .map(|a| (a.attempt_id.as_str(), a))
889        .collect();
890    let effects = presented
891        .into_iter()
892        .map(|p| {
893            let dispatch = match attempts.get(p.attempt_id.as_str()) {
894                Some(a) => Dispatch::Dispatched {
895                    end: a.end.clone(),
896                    cancellation: a.cancellation,
897                    remote: a.remote,
898                    charge: a.cost.charge,
899                },
900                None => Dispatch::NotDispatched,
901            };
902            LiveEffect {
903                step: p.step,
904                instance: p.instance,
905                target: p.target,
906                role: p.role,
907                attempt_id: p.attempt_id,
908                backend_id: p.backend_id,
909                artifact: p.artifact,
910                request: p.request,
911                presented: p.presented,
912                exposure: p.exposure,
913                remaining_ms: p.remaining_ms,
914                authorization: p.authorization,
915                dispatch,
916            }
917        })
918        .collect();
919    let unreconciled = attempts
920        .values()
921        .filter(|a| a.cost.charge == Charge::Unknown)
922        .map(|a| a.attempt_id.clone())
923        .collect();
924    let delivery = match delivery {
925        Ok(Delivery::Acknowledged { receipt }) => DeliveryObservation::Acknowledged {
926            receipt: cut(receipt, DETAIL_BYTES),
927        },
928        Ok(Delivery::Queued(_)) => DeliveryObservation::Queued,
929        Ok(Delivery::Dropped { dropped_total }) => DeliveryObservation::Dropped {
930            dropped_total: *dropped_total,
931        },
932        Err(f) => DeliveryObservation::NotDelivered {
933            detail: cut(&f.detail, DETAIL_BYTES),
934            uncertain: f.uncertain,
935        },
936    };
937    let capture = match (&req.capture, capture) {
938        (Some(c), Some(CaptureOutcome::Captured(k))) => CapturePosture::On {
939            scope: c.scope.clone(),
940            max_bytes: c.max_bytes,
941            status: k.status,
942        },
943        _ => CapturePosture::Off,
944    };
945    LiveReexecution {
946        schema: schema::LIVE_REEXECUTION.into(),
947        decision_id: req.decision_id.clone(),
948        plan_id: record.plan_id.clone(),
949        snapshot_id,
950        evaluation_time_ms: req.evaluation_time,
951        subject: req.subject.clone(),
952        scope: req.scope.clone(),
953        historical: match &req.historical {
954            Some(h) => Historical::Linked(h.clone()),
955            None => Historical::None,
956        },
957        admission: Admission {
958            queued_ms: record.timing.queued_ms,
959            deadline_ms: record.timing.deadline_ms,
960        },
961        run_digest: record.record_digest().ok(),
962        run: RunLink {
963            termination: record.termination,
964            cost: record.cost,
965        },
966        effects,
967        unreconciled,
968        delivery,
969        retention: RetentionPosture {
970            capture,
971            historical: if req.historical.is_some() {
972                HistoricalRetention::NotObserved
973            } else {
974                HistoricalRetention::NoHistory
975            },
976        },
977        comparison: ComparisonSection::NotRequested,
978    }
979}