Skip to main content

rite_runtime/
runner.rs

1//! Channel-driven ceremony executor built around the protocol vocabulary.
2//!
3//! Frontends (TUI, console, headless) interact with the executor
4//! exclusively through [`UiCommand`] and [`ExecEvent`]; the transcript is
5//! recorded inline as facts are emitted.
6//!
7//! # Architecture
8//!
9//! ```text
10//!         ┌──────────────────────────┐
11//!         │      Frontend thread     │
12//!         │  (TUI / console / etc.)  │
13//!         └──────────────────────────┘
14//!           ▲                   │
15//!  ExecEvent│                   │UiCommand
16//!           │                   ▼
17//!         ┌──────────────────────────┐
18//!         │      Executor thread     │
19//!         │  ┌────────────────────┐  │
20//!         │  │      Reporter      │──┼─→ TranscriptSink (inline)
21//!         │  └────────────────────┘  │
22//!         │      ▲                   │
23//!         │      │ reporter.fact()   │
24//!         │  ┌────────────────────┐  │
25//!         │  │   Action handlers  │  │
26//!         │  └────────────────────┘  │
27//!         └──────────────────────────┘
28//! ```
29//!
30//! The executor owns the [`TranscriptSink`] for the duration of the run and
31//! finalizes it before returning. Action handlers receive a `&mut Reporter`
32//! and emit transcript-worthy facts through it.
33
34use std::collections::{HashMap, HashSet};
35use std::sync::Arc;
36
37use crossbeam_channel::{Receiver, Sender};
38use rite_model::{
39    ActId, ActionType, ArtifactId, Ceremony, Level, MaterialId, MaterialKind, MaterialSource,
40    OutputId, ParamId, RoleId, Sha256Digest, Step, TranscriptHeader,
41};
42use rite_sdk::{Backend, BackendError, Retriability};
43use thiserror::Error;
44
45use crate::actions::ArtifactValue;
46use crate::backend::BackendRegistry;
47use crate::clock::{Clock, SystemClock};
48use crate::entropy::DERIVATION_V1;
49use crate::executor::{
50    ExecutionError, load_material_artifact, step_info_from, write_artifact_to_disk,
51};
52use crate::expressions;
53use crate::output_config::OutputConfig;
54use crate::protocol::{ExecEvent, MaterialOverview, Response, UiCommand, UiSignal};
55use crate::reporter::{Reporter, ReporterError};
56use crate::state::{ExecutionState, HandlerContext, StepResult};
57use crate::step_info::StepInfo;
58use crate::system_info::StartupSnapshot;
59use crate::transcript_sink::{TranscriptFingerprint, TranscriptSink};
60use rite_model::validate::Values;
61use rite_model::{ErrorClass, ErrorRecord, Prompt, RetryPolicy, StepFact, StepOutcome};
62
63/// Gather the machine entropy `m` that seeds the ceremony entropy source.
64///
65/// Sourced directly from the host OS RNG, independent of any
66/// ceremony backend, so a device the ceremony later challenges cannot
67/// influence its own challenge nonce. A dry run instead returns a fixed,
68/// clearly-labelled sentinel so a re-derived value can never be mistaken for
69/// one produced under real entropy.
70fn gather_machine_entropy(dry_run: bool) -> Result<([u8; 32], String), ExecutionError> {
71    if dry_run {
72        let mut m = [0u8; 32];
73        for (slot, byte) in m.iter_mut().zip(b"rite-dry-run-not-real-entropy") {
74            *slot = *byte;
75        }
76        return Ok((m, "dry-run".to_string()));
77    }
78    let m =
79        crate::os_random::os_random().map_err(|e| ExecutionError::EntropyError(e.to_string()))?;
80    Ok((m, "os".to_string()))
81}
82
83/// Name and version of the program writing transcripts, as the header
84/// records it.
85pub const PRODUCER: &str = concat!("rite ", env!("CARGO_PKG_VERSION"));
86
87/// A fresh run identifier: 16 bytes from the OS RNG, as lowercase hex.
88fn new_run_id() -> Result<String, ExecutionError> {
89    let id: [u8; 16] =
90        crate::os_random::os_random().map_err(|e| ExecutionError::EntropyError(e.to_string()))?;
91    Ok(base16ct::lower::encode_string(&id))
92}
93
94/// Errors that may surface from an [`Action`] handler.
95#[derive(Debug, Error)]
96pub enum ActionError {
97    /// Operator requested abort. Propagated from [`Reporter::check_abort`]
98    /// or [`Reporter::prompt`].
99    #[error("ceremony aborted by operator")]
100    Aborted,
101    /// Frontend channel disconnected. The executor unwinds to a failure.
102    #[error("frontend channel disconnected")]
103    Disconnected,
104    /// Transcript sink failed to persist a fact.
105    #[error("transcript write failed: {0}")]
106    Transcript(std::io::Error),
107    /// Underlying backend returned an error.
108    #[error("backend error: {0}")]
109    Backend(#[from] BackendError),
110    /// Catch-all for handler-specific failures.
111    #[error("{0}")]
112    Failed(String),
113}
114
115impl From<ReporterError> for ActionError {
116    fn from(value: ReporterError) -> Self {
117        match value {
118            ReporterError::Aborted => ActionError::Aborted,
119            ReporterError::Disconnected => ActionError::Disconnected,
120            ReporterError::Transcript(e) => ActionError::Transcript(e),
121            ReporterError::NoCurrentStep(_)
122            | ReporterError::DuplicateDraw { .. }
123            | ReporterError::Unseeded
124            | ReporterError::DrawTooLong { .. } => ActionError::Failed(value.to_string()),
125        }
126    }
127}
128
129impl ActionError {
130    /// Classify whether the step that failed with this error may be retried.
131    ///
132    /// Delegates to [`BackendError::retriability`] for [`ActionError::Backend`];
133    /// every other handler-level error is [`Retriability::Fatal`]. `Failed`
134    /// covers procedural failures (a `check_value` mismatch must never be
135    /// re-run, that would be tampering) as well as integrity and configuration
136    /// errors, all of which are non-retriable. `Aborted` is an operator decision
137    /// handled on the abort path and is never auto-retried, so it is classified
138    /// fatal here as a safe default.
139    #[must_use]
140    pub fn retriability(&self) -> Retriability {
141        match self {
142            ActionError::Backend(e) => e.retriability(),
143            ActionError::Aborted
144            | ActionError::Disconnected
145            | ActionError::Transcript(_)
146            | ActionError::Failed(_) => Retriability::Fatal,
147        }
148    }
149}
150
151/// Deserialize the per-step parameter blob into a typed handler-specific struct.
152///
153/// Wraps `serde_json::from_value` and converts the error into
154/// [`ActionError::Failed`] with a consistent "invalid params: …" wording.
155///
156/// # Errors
157///
158/// Returns [`ActionError::Failed`] if `params` cannot be deserialized into `P`.
159pub fn parse_params<P: serde::de::DeserializeOwned>(
160    params: &serde_json::Value,
161) -> Result<P, ActionError> {
162    serde_json::from_value(params.clone())
163        .map_err(|e| ActionError::Failed(format!("invalid params: {e}")))
164}
165
166impl From<ReporterError> for ExecutionError {
167    fn from(value: ReporterError) -> Self {
168        match value {
169            ReporterError::Aborted => ExecutionError::Aborted,
170            ReporterError::Disconnected => {
171                ExecutionError::TranscriptError("frontend channel disconnected".to_string())
172            }
173            ReporterError::Transcript(e) => ExecutionError::TranscriptError(e.to_string()),
174            ReporterError::NoCurrentStep(_) => ExecutionError::TranscriptError(value.to_string()),
175            ReporterError::DuplicateDraw { .. }
176            | ReporterError::Unseeded
177            | ReporterError::DrawTooLong { .. } => ExecutionError::EntropyError(value.to_string()),
178        }
179    }
180}
181
182/// Action trait, every ceremony step handler implements this.
183///
184/// Handlers emit facts and prompts through [`Reporter`] and return a
185/// [`StepResult`]. Transcript evidence is whatever facts the handler
186/// emits via [`Reporter::fact`] during execution; there is no separate
187/// evidence struct.
188pub trait Action: Send + Sync {
189    /// The action this handler implements, as the DSL names it.
190    fn action_type(&self) -> ActionType;
191
192    /// Fill in per-step parameter defaults. The executor calls this on a
193    /// block the action's schema accepted, so `params` is an object.
194    fn apply_defaults(&self, _params: &mut serde_json::Value, _step: &StepInfo) {}
195
196    /// Report values in a step's `with:` block that *this build* cannot carry
197    /// out, without executing the step.
198    ///
199    /// Only build-relative findings belong here. Whether a value is one the
200    /// action accepts at all is settled by the action's schema, which the
201    /// resolver checks, so that an editor linking no backend reports it too.
202    /// What is left for a handler is the question only a linked binary can
203    /// answer: whether the feature, backend or library that would carry the
204    /// value out is present.
205    ///
206    /// `params` holds the literal part of the block, as produced by
207    /// [`literal_expr_value`](rite_model::expression::literal_expr_value): a
208    /// field whose value is an unevaluated expression is absent, because its
209    /// value belongs to run time. Never report a field as missing.
210    fn unsupported_params(&self, _params: &serde_json::Value, _step: &StepInfo) -> Vec<String> {
211        Vec::new()
212    }
213
214    /// Execute the action.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`ActionError`] when the handler cannot complete, when the
219    /// operator aborts, or when the frontend disappears.
220    fn execute(
221        &self,
222        step: &StepInfo,
223        ctx: &HandlerContext,
224        params: &serde_json::Value,
225        reporter: &mut Reporter<'_>,
226        backend: Option<&mut dyn Backend>,
227    ) -> Result<StepResult, ActionError>;
228}
229
230/// A `with:` value this build cannot carry out, and the step holding it.
231///
232/// The same distinction [`ActionRegistry::unsupported_actions`] rests on: a
233/// definition is validated on one machine and executed on another, so a
234/// finding that depends on the running binary must not condemn the document.
235/// `rite check` warns; `rite run` refuses, because that process is the one
236/// that would have to do the work.
237#[derive(Debug, Clone)]
238pub struct StepUnsupportedParam {
239    /// The step whose `with:` block holds the value.
240    pub step: rite_model::StepId,
241    /// What this build cannot do, phrased for the ceremony author.
242    pub message: String,
243}
244
245/// Registry mapping action types to their handlers.
246#[derive(Default)]
247pub struct ActionRegistry {
248    actions: HashMap<ActionType, Arc<dyn Action>>,
249}
250
251impl ActionRegistry {
252    /// Create an empty registry.
253    #[must_use]
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// Register an action handler.
259    pub fn register(&mut self, action: Arc<dyn Action>) {
260        let action_type = action.action_type();
261        self.actions.insert(action_type, action);
262    }
263
264    /// Look up the handler for an action type.
265    #[must_use]
266    pub fn get(&self, action: &ActionType) -> Option<&Arc<dyn Action>> {
267        self.actions.get(action)
268    }
269
270    /// Iterate over registered action types.
271    pub fn action_types(&self) -> impl Iterator<Item = &ActionType> {
272        self.actions.keys()
273    }
274
275    /// Return action types used in the execution plan that have no
276    /// registered handler. Results are deduplicated and returned in
277    /// first-occurrence order.
278    #[must_use]
279    pub fn unsupported_actions(&self, steps: &[rite_model::Step]) -> Vec<ActionType> {
280        let mut seen = std::collections::HashSet::new();
281        let mut result = Vec::new();
282        for step in steps {
283            if !self.actions.contains_key(&step.action) && seen.insert(step.action) {
284                result.push(step.action);
285            }
286        }
287        result
288    }
289
290    /// Ask each step's handler what this build cannot carry out.
291    ///
292    /// Handlers see the literal part of the block with defaults applied, the
293    /// same order the executor uses. A step whose action has no handler in
294    /// this build is skipped: [`unsupported_actions`](Self::unsupported_actions)
295    /// already reports it, and there is nobody to ask. A step whose whole
296    /// `with:` block is an expression is skipped too, since none of it is
297    /// known yet.
298    #[must_use]
299    pub fn unsupported_step_params(&self, steps: &[rite_model::Step]) -> Vec<StepUnsupportedParam> {
300        let mut found = Vec::new();
301        for step in steps {
302            let Some(handler) = self.actions.get(&step.action) else {
303                continue;
304            };
305            let Some(mut params) = rite_model::expression::literal_expr_value(&step.with) else {
306                continue;
307            };
308            let step_info = crate::executor::step_info_from(step);
309            handler.apply_defaults(&mut params, &step_info);
310            found.extend(
311                handler
312                    .unsupported_params(&params, &step_info)
313                    .into_iter()
314                    .map(|message| StepUnsupportedParam {
315                        step: step.id.clone(),
316                        message,
317                    }),
318            );
319        }
320        found
321    }
322}
323
324/// Summary returned by [`Executor::run`] on a successful completion.
325#[derive(Debug)]
326pub struct ExecutionSummary {
327    /// Ceremony name from the DSL.
328    pub ceremony_name: String,
329    /// Number of steps that completed successfully.
330    pub steps_completed: usize,
331    /// Final transcript fingerprint.
332    pub transcript_fingerprint: TranscriptFingerprint,
333}
334
335/// Channel-driven executor. One per ceremony run.
336pub struct Executor {
337    ceremony: Ceremony,
338    registry: ActionRegistry,
339    backend_registry: BackendRegistry,
340    output_config: OutputConfig,
341    dry_run: bool,
342    startup: StartupSnapshot,
343    clock: Arc<dyn Clock>,
344}
345
346impl Executor {
347    /// Build an executor for a single ceremony run.
348    ///
349    /// `startup` carries the system identity and device environment the CLI
350    /// gathered at launch; the executor echoes them to the frontend as UI
351    /// signals at ceremony start and does not otherwise inspect them.
352    #[must_use]
353    pub fn new(
354        ceremony: Ceremony,
355        registry: ActionRegistry,
356        backend_registry: BackendRegistry,
357        output_config: OutputConfig,
358        dry_run: bool,
359        startup: StartupSnapshot,
360    ) -> Self {
361        Self {
362            ceremony,
363            registry,
364            backend_registry,
365            output_config,
366            dry_run,
367            startup,
368            clock: Arc::new(SystemClock),
369        }
370    }
371
372    /// Override the run clock. The default is [`SystemClock`]; tests inject a
373    /// fixed or stepping clock to make recorded event times deterministic.
374    #[must_use]
375    pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
376        self.clock = clock;
377        self
378    }
379
380    /// Drive the ceremony to completion.
381    ///
382    /// Blocking. Intended to be invoked on a dedicated executor thread
383    /// spawned by the frontend. The `transcript_sink` is finalized before
384    /// this function returns, and its fingerprint is included in the
385    /// returned summary.
386    ///
387    /// # Errors
388    ///
389    /// Returns [`ExecutionError`] for any condition that prevents the
390    /// ceremony from completing, validation failures, missing actions,
391    /// abort, backend errors, or transcript I/O failures.
392    pub fn run(
393        self,
394        cmd_rx: &Receiver<UiCommand>,
395        event_tx: &Sender<ExecEvent>,
396        mut transcript_sink: Box<dyn TranscriptSink>,
397    ) -> Result<ExecutionSummary, ExecutionError> {
398        validate_parameters(&self.ceremony)?;
399
400        // The header goes first: it says what the file is before any fact.
401        let header = TranscriptHeader::new(PRODUCER, &new_run_id()?, self.dry_run);
402        transcript_sink
403            .begin(&header)
404            .map_err(|e| ExecutionError::TranscriptError(e.to_string()))?;
405
406        let resolved_params: HashMap<ParamId, serde_json::Value> = self
407            .ceremony
408            .parameters
409            .iter()
410            .map(|(id, p)| (id.clone(), p.value.clone()))
411            .collect();
412
413        let roles: HashMap<RoleId, String> = self
414            .ceremony
415            .roles
416            .iter()
417            .map(|(id, r)| (id.clone(), r.name.clone()))
418            .collect();
419
420        let materials_map: HashMap<MaterialId, String> = self
421            .ceremony
422            .materials
423            .iter()
424            .map(|(id, m)| (id.clone(), m.display_name().to_string()))
425            .collect();
426
427        let ceremony_name = self.ceremony.metadata.name.clone();
428        // Keep a clock handle for the terminal facts below: `execute_inner`
429        // consumes `self`, so the reporter and the runner share this clone.
430        let clock = Arc::clone(&self.clock);
431        let mut reporter = Reporter::new(
432            event_tx,
433            cmd_rx,
434            transcript_sink.as_mut(),
435            Arc::clone(&clock),
436        );
437
438        let outcome = self.execute_inner(&mut reporter, resolved_params, roles, materials_map);
439
440        // Record the terminal fact first so it ends up on disk. Then
441        // finalize the sink (which only returns the chain head, no
442        // sidecar is written) and forward both the fact and the
443        // fingerprint to the UI.
444        match outcome {
445            Ok(counts) => {
446                let at = clock.now();
447                let completed = StepFact::CeremonyCompleted {};
448                transcript_sink
449                    .record(at, completed.default_level(), &completed)
450                    .map_err(|e| ExecutionError::TranscriptError(e.to_string()))?;
451                let _ = event_tx.send(ExecEvent::Fact {
452                    at,
453                    fact: completed,
454                });
455                let fingerprint = transcript_sink
456                    .finalize()
457                    .map_err(|e| ExecutionError::TranscriptError(e.to_string()))?;
458                let _ = event_tx.send(ExecEvent::Finalized {
459                    fingerprint: fingerprint.as_str().to_string(),
460                });
461                Ok(ExecutionSummary {
462                    ceremony_name,
463                    steps_completed: counts.completed,
464                    transcript_fingerprint: fingerprint,
465                })
466            }
467            Err(err) => {
468                let at = clock.now();
469                let record = err.to_error_record();
470                let failed = StepFact::CeremonyFailed { error: record };
471                let _ = transcript_sink.record(at, failed.default_level(), &failed);
472                let _ = event_tx.send(ExecEvent::Fact { at, fact: failed });
473                if let Ok(fingerprint) = transcript_sink.finalize() {
474                    let _ = event_tx.send(ExecEvent::Finalized {
475                        fingerprint: fingerprint.as_str().to_string(),
476                    });
477                }
478                Err(err)
479            }
480        }
481    }
482
483    #[allow(clippy::too_many_lines)]
484    fn execute_inner(
485        mut self,
486        reporter: &mut Reporter<'_>,
487        resolved_params: HashMap<ParamId, serde_json::Value>,
488        roles: HashMap<RoleId, String>,
489        materials_map: HashMap<MaterialId, String>,
490    ) -> Result<StepCounts, ExecutionError> {
491        let dry_run = self.dry_run;
492
493        reporter.fact(StepFact::CeremonyStarted {
494            name: self.ceremony.metadata.name.clone(),
495            template: self.ceremony.source_digest.clone(),
496        })?;
497
498        // Establish the ceremony entropy source before any step can draw from
499        // it. This machine seed is run-metadata (the runner-emitted exception
500        // to "facts come from actions"); human contributions, by contrast, are
501        // authored `gather_entropy` steps that fold into the ratchet later.
502        let (m, source) = gather_machine_entropy(dry_run)?;
503        reporter.seed_entropy(&m);
504        reporter.fact(StepFact::EntropySeeded {
505            m: base16ct::lower::encode_string(&m),
506            source,
507            derivation: DERIVATION_V1.to_string(),
508        })?;
509
510        // The inputs this run was given, one fact each so each can be
511        // disclosed on its own. The template digest on `CeremonyStarted`
512        // covers none of them.
513        for (id, role) in self.ceremony.roles.iter() {
514            reporter.fact(StepFact::RoleDeclared {
515                role: id.clone(),
516                name: role.name.clone(),
517            })?;
518        }
519        for (id, role) in self.ceremony.roles.iter() {
520            if let Some(person) = &role.person {
521                reporter.fact(StepFact::RoleAssigned {
522                    role: id.clone(),
523                    person: person.clone(),
524                })?;
525            }
526        }
527        for (id, parameter) in self.ceremony.parameters.iter() {
528            reporter.fact(StepFact::ParameterBound {
529                name: id.clone(),
530                value: parameter.value.clone(),
531            })?;
532        }
533
534        // Pre-ceremony overview: descriptive metadata for the UI's
535        // Overview screen. Sent as a UI-only signal, not a transcript
536        // fact, because the YAML is the source of truth for these fields
537        // and the run record shouldn't duplicate them.
538        reporter.signal(UiSignal::CeremonyOverview {
539            description: self.ceremony.metadata.description.clone(),
540            materials: self
541                .ceremony
542                .materials
543                .iter()
544                .map(|(_, m)| MaterialOverview::from_material(m))
545                .collect(),
546            step_count: self.ceremony.execution_plan.len(),
547        })?;
548
549        // Machine identity and device environment for the System tab. Both
550        // UI-only: identity that belongs in the transcript is recorded by the
551        // `machine_info` action, not fed in here. `Environment` is emitted
552        // once today but shaped to be re-emitted by a future live observer.
553        reporter.signal(UiSignal::SystemInfo(Box::new(self.startup.system.clone())))?;
554        reporter.signal(UiSignal::Environment(self.startup.environment.clone()))?;
555
556        // Ceremony-start gate: let the operator review the overview
557        // before any side effect (material loading, first step body). The
558        // prompt is emitted with no current step set, so it carries
559        // `step: None` through to the frontend.
560        if !dry_run {
561            reporter.prompt(&Prompt::Continue {
562                hint: Some("Press Enter to start the ceremony".to_string()),
563            })?;
564        }
565
566        let mut state = ExecutionState::new(resolved_params, roles, materials_map, dry_run);
567
568        // Load materials. The digest of a file is its own fact, so an
569        // audience can learn that an input was loaded without being able to
570        // test guesses against its content.
571        for (id, material) in self.ceremony.materials.iter() {
572            let artifact = load_material_artifact(id.as_str(), material)?;
573            let identifier = match &material.kind {
574                MaterialKind::Physical { identifier, .. } => identifier.clone(),
575                MaterialKind::Digital {
576                    source: Some(MaterialSource::Identifier { identifier }),
577                } => Some(identifier.clone()),
578                MaterialKind::Digital { .. } => None,
579            };
580            // A file loads as bytes; an identifier or physical item as text.
581            let digest = match &artifact {
582                ArtifactValue::Bytes(bytes) => Some(Sha256Digest::of(bytes)),
583                _ => None,
584            };
585            reporter.fact(StepFact::MaterialLoaded {
586                name: id.clone(),
587                identifier,
588            })?;
589            if let Some(digest) = digest {
590                reporter.fact(StepFact::MaterialDigest {
591                    name: id.clone(),
592                    digest,
593                })?;
594            }
595            state = state.with_material(ArtifactId::new(id.as_str()), artifact);
596        }
597
598        let mut backends = RunBackends {
599            registry: &mut self.backend_registry,
600            bound: HashSet::new(),
601        };
602        let has_acts = !self.ceremony.acts.is_empty();
603        let mut current_act: Option<ActId> = None;
604        let mut counts = StepCounts::default();
605
606        // Move the plan out so we can iterate it while still borrowing
607        // other fields of `self.ceremony` (sections, acts, outputs).
608        let plan = std::mem::take(&mut self.ceremony.execution_plan);
609        for step in &plan {
610            // Act boundary
611            if has_acts {
612                let step_act = self
613                    .ceremony
614                    .sections
615                    .get(&step.section)
616                    .and_then(|s| s.act.clone());
617                if step_act != current_act {
618                    if let Some(act_id) = &step_act
619                        && let Some(act) = self.ceremony.acts.get(act_id)
620                    {
621                        reporter.fact(StepFact::ActStarted {
622                            id: act_id.clone(),
623                            label: act
624                                .name
625                                .clone()
626                                .unwrap_or_else(|| act_id.as_str().to_string()),
627                        })?;
628                    }
629                    current_act = step_act;
630                }
631            }
632
633            reporter.set_current_step(Some(step.id.clone()));
634            reporter.set_current_backend(step.backend.clone());
635            reporter.fact(StepFact::StepStarted {
636                id: step.id.clone(),
637                label: step.step_label.clone(),
638                role: step.role.clone().unwrap_or_else(|| RoleId::new("")),
639            })?;
640
641            // Look up handler
642            let handler = self
643                .registry
644                .get(&step.action)
645                .ok_or(ExecutionError::UnknownAction(step.action))?
646                .clone();
647
648            // The params depend only on what earlier steps produced, so a
649            // block that cannot run is refused before the operator is asked
650            // to start the step.
651            let ctx = state.handler_context();
652            let step_info = step_info_from(step);
653            let params = step_params(handler.as_ref(), step, &step_info, &ctx, reporter)?;
654
655            // Pacing: gate the step body on operator acknowledgement. The
656            // prompt fires with the step header already visible, so the
657            // operator confirms "ready for step X" rather than "what just
658            // happened was fine". `silent` skips it for auto-advancing
659            // bookkeeping steps; `dry_run` skips it for non-interactive
660            // verification.
661            if !step.silent && !dry_run {
662                reporter.prompt(&Prompt::Continue {
663                    hint: Some(format!("Press Enter to start step {}", step.step_label)),
664                })?;
665            }
666
667            let result = execute_step_with_retry(
668                handler.as_ref(),
669                step,
670                &step_info,
671                &ctx,
672                &params,
673                reporter,
674                &mut backends,
675            )?;
676
677            // StepCompleted
678            reporter.fact(StepFact::StepCompleted {
679                id: step.id.clone(),
680                outcome: result.outcome.clone(),
681            })?;
682
683            if let StepOutcome::Completed { .. } = &result.outcome {
684                counts.completed = counts.completed.saturating_add(1);
685            }
686
687            state = merge_step_result(state, result);
688
689            // Write output-bound artifacts. The DSL declares which artifact
690            // a step `creates`; we promote that to disk if it is also a
691            // ceremony-level output.
692            if let Some(artifact_id) = &step.creates
693                && !dry_run
694            {
695                let output_id = OutputId::new(artifact_id.as_str());
696                if self.ceremony.outputs.contains(&output_id) {
697                    std::fs::create_dir_all(self.output_config.artifacts_dir()).map_err(|e| {
698                        ExecutionError::OutputWriteFailed {
699                            name: "artifacts directory".to_string(),
700                            reason: e.to_string(),
701                        }
702                    })?;
703
704                    let artifact_value = state.artifacts.get(artifact_id).ok_or_else(|| {
705                        ExecutionError::OutputWriteFailed {
706                            name: artifact_id.as_str().to_string(),
707                            reason: "artifact not produced".to_string(),
708                        }
709                    })?;
710
711                    let (path, digest, _size, _mime_type) =
712                        write_artifact_to_disk(artifact_id, artifact_value, &self.output_config)?;
713
714                    // Only the file name is recorded: the file lives under
715                    // the run's `artifacts/` directory, and the operator's
716                    // filesystem layout stays out of the transcript.
717                    let file = path
718                        .file_name()
719                        .and_then(|name| name.to_str())
720                        .ok_or_else(|| ExecutionError::OutputWriteFailed {
721                            name: artifact_id.as_str().to_string(),
722                            reason: format!("'{}' has no UTF-8 file name", path.display()),
723                        })?
724                        .to_string();
725
726                    // Opened content gets no digest: a digest of a secret can
727                    // be tested against guesses.
728                    let digest = match artifact_value {
729                        ArtifactValue::Secret(_) => None,
730                        _ => Some(digest),
731                    };
732
733                    reporter.fact_at(
734                        artifact_level(step.action, artifact_value),
735                        StepFact::ArtifactWritten {
736                            step: step.id.clone(),
737                            name: artifact_id.as_str().to_string(),
738                            file,
739                            digest,
740                        },
741                    )?;
742                }
743            }
744        }
745
746        reporter.set_current_step(None);
747        reporter.set_current_backend(None);
748        drop(plan);
749
750        Ok(counts)
751    }
752}
753
754#[derive(Default, Debug, Clone, Copy)]
755struct StepCounts {
756    completed: usize,
757}
758
759/// The level an artifact is recorded at, which decides whether a disclosure
760/// carries its file.
761///
762/// Certificates and public keys exist to be distributed. Bytes are public
763/// only from the actions that make distributable bytes, a CSR or a signature;
764/// from any other step they may be a document the ceremony handled, so they
765/// stay restricted. Wrapped keys and encrypted content are ciphertext: not
766/// secret, and not for everyone either. Opened content is confidential and is
767/// never bundled at all.
768fn artifact_level(action: ActionType, value: &ArtifactValue) -> Level {
769    match value {
770        ArtifactValue::Certificate(_) | ArtifactValue::PublicKey(_) => Level::PUBLIC,
771        ArtifactValue::Bytes(_)
772            if matches!(
773                action,
774                ActionType::GenerateCsr | ActionType::SignData | ActionType::PivSign
775            ) =>
776        {
777            Level::PUBLIC
778        }
779        ArtifactValue::Bytes(_)
780        | ArtifactValue::Text(_)
781        | ArtifactValue::WrappedKey(_)
782        | ArtifactValue::EncryptedData(_)
783        | ArtifactValue::BackendKey { .. }
784        | ArtifactValue::Shares(_) => Level::RESTRICTED,
785        ArtifactValue::Secret(_) => Level::CONFIDENTIAL,
786    }
787}
788
789/// The run's backends, and which of them have had their identity recorded.
790struct RunBackends<'a> {
791    registry: &'a mut BackendRegistry,
792    bound: HashSet<String>,
793}
794
795impl RunBackends<'_> {
796    /// Hand out a backend, recording its identity the first time it is
797    /// acquired. Operations name the backend and do not repeat the identity,
798    /// which can carry device serials.
799    fn acquire(
800        &mut self,
801        name: &str,
802        reporter: &mut Reporter<'_>,
803    ) -> Result<&mut dyn Backend, ActionError> {
804        let backend = self.registry.get_mut(name)?;
805        if self.bound.insert(name.to_string()) {
806            reporter.fact(StepFact::BackendBound {
807                name: name.to_string(),
808                provider: backend.provider().to_string(),
809                identity: backend.fingerprint(),
810            })?;
811        }
812        Ok(backend)
813    }
814}
815
816/// Check the evaluated `with:` block against the action's schema, and the
817/// rules over several values that a schema cannot say.
818///
819/// The resolver checked what was literal; an expression's value is known
820/// only now. It is refused under the rule a literal one would break, before
821/// the handler or a backend sees it, so a handler can rely on its params
822/// having the shape the schema describes. Every value is known here, so
823/// nothing is deferred.
824///
825/// The value is evaluated, and may be a secret an expression copied in, so
826/// the message names the field and the rule and never the value: it reaches
827/// the transcript and the screen.
828fn check_params(action: ActionType, params: &serde_json::Value) -> Result<(), String> {
829    let check = rite_model::validate::params_check(action).map_err(|error| error.to_string())?;
830    let mut messages: Vec<String> = Vec::new();
831    let found = check
832        .validate(params)
833        .into_iter()
834        .map(|violation| check.message(action, params, &violation, Values::Withheld))
835        .chain(
836            rite_model::params::cross_field(action, params)
837                .into_iter()
838                .map(|error| error.message),
839        );
840    for message in found {
841        if !messages.contains(&message) {
842            messages.push(message);
843        }
844    }
845    if messages.is_empty() {
846        Ok(())
847    } else {
848        Err(messages.join("; "))
849    }
850}
851
852/// The step's params: its `with:` block evaluated, checked, and with the
853/// handler's defaults filled in.
854///
855/// A refusal is recorded as the step's first attempt failing, and never
856/// retried, since the params are the same on every attempt. The check sees
857/// the block as the definition wrote it, as the resolver does; the defaults
858/// are filled in after.
859fn step_params(
860    handler: &dyn Action,
861    step: &Step,
862    step_info: &StepInfo,
863    ctx: &HandlerContext,
864    reporter: &mut Reporter<'_>,
865) -> Result<serde_json::Value, ExecutionError> {
866    let mut params = expressions::evaluate_expr_value(&step.with, ctx)?;
867    // A step with no `with:` has no parameters, as the resolver reads it.
868    if params.is_null() {
869        params = serde_json::Value::Object(serde_json::Map::new());
870    }
871    if let Err(reason) = check_params(step.action, &params) {
872        let exec_err = ExecutionError::InvalidParams(format!("step '{}': {reason}", step.id));
873        reporter.fact(StepFact::StepAttemptFailed {
874            step: step.id.clone(),
875            attempt: 1,
876            error: exec_err.to_error_record(),
877        })?;
878        return Err(exec_err);
879    }
880    handler.apply_defaults(&mut params, step_info);
881    Ok(params)
882}
883
884/// Run one step, retrying transient failures under operator control.
885///
886/// Each attempt re-acquires the backend handle (it is moved into the handler)
887/// and snapshots the side-effect count beforehand: a transient error is
888/// retriable only if the attempt performed no work on the world yet (the
889/// conservative re-executability gate). Interaction records such as an
890/// answered prompt do not close the gate: a retried attempt simply prompts
891/// again. A retriable error within the step's [`RetryPolicy`] prompts the
892/// operator; any other failure terminates the run.
893fn execute_step_with_retry(
894    handler: &dyn Action,
895    step: &Step,
896    step_info: &StepInfo,
897    ctx: &HandlerContext,
898    params: &serde_json::Value,
899    reporter: &mut Reporter<'_>,
900    backends: &mut RunBackends<'_>,
901) -> Result<StepResult, ExecutionError> {
902    let mut attempt: u32 = 1;
903    loop {
904        let side_effects_before = reporter.side_effects_emitted();
905
906        // Backend acquisition is part of the attempt: a failed lazy
907        // initialization (device unplugged, daemon down) carries the same
908        // retriability semantics as a backend error inside the handler.
909        let attempt_result = match &step_info.backend {
910            Some(name) => backends.acquire(name, reporter).and_then(|backend| {
911                handler.execute(step_info, ctx, params, reporter, Some(backend))
912            }),
913            None => handler.execute(step_info, ctx, params, reporter, None),
914        };
915
916        let action_err = match attempt_result {
917            Ok(result) => return Ok(result),
918            // Abort is an operator decision, not an attempt failure: no
919            // StepAttemptFailed is recorded, the run terminates here.
920            Err(ActionError::Aborted) => return Err(ExecutionError::Aborted),
921            Err(action_err) => action_err,
922        };
923
924        // Conservative re-executability gate: a step that already performed
925        // work on the world this attempt is not safely re-runnable.
926        let no_new_side_effects = reporter.side_effects_emitted() == side_effects_before;
927        let retriable = action_err.retriability().is_retriable() && no_new_side_effects;
928        let exec_err = action_error_to_execution(action_err, &step.id);
929        reporter.fact(StepFact::StepAttemptFailed {
930            step: step.id.clone(),
931            attempt,
932            error: exec_err.to_error_record(),
933        })?;
934
935        let policy_allows = match step.retry {
936            RetryPolicy::Never => false,
937            RetryPolicy::MaxAttempts(max) => attempt < max,
938            RetryPolicy::Prompt => true,
939        };
940
941        if retriable && policy_allows {
942            // Default `false`: the headless/dry-run driver answers the default,
943            // so non-interactive runs abort rather than loop on a deterministic
944            // error.
945            let answer = reporter.prompt(&Prompt::Confirm {
946                question: format!("Step {} failed: {exec_err}. Retry?", step.step_label),
947                default: Some(false),
948            })?;
949            if matches!(answer, Response::Bool(true)) {
950                attempt = attempt.saturating_add(1);
951                continue;
952            }
953        }
954        return Err(exec_err);
955    }
956}
957
958/// Map a handler-level [`ActionError`] to the execution-level error the run
959/// terminates with. `Aborted` is included for completeness; the executor
960/// special-cases it before calling this so it never records a step-attempt
961/// failure for an abort.
962fn action_error_to_execution(err: ActionError, step_id: &rite_model::StepId) -> ExecutionError {
963    match err {
964        ActionError::Aborted => ExecutionError::Aborted,
965        ActionError::Disconnected => ExecutionError::StepFailed {
966            step: step_id.clone(),
967            reason: "frontend channel disconnected".to_string(),
968        },
969        ActionError::Transcript(e) => ExecutionError::TranscriptError(e.to_string()),
970        ActionError::Backend(e) => ExecutionError::BackendError(e),
971        ActionError::Failed(reason) => ExecutionError::StepFailed {
972            step: step_id.clone(),
973            reason,
974        },
975    }
976}
977
978impl ExecutionError {
979    /// Convert this execution error into a transcript-friendly record.
980    ///
981    /// The audit `class` derives from the error's nature; for a backend error
982    /// it reuses the same retriability judgment the runtime uses for retry
983    /// (a retriable backend error is environmental).
984    fn to_error_record(&self) -> ErrorRecord {
985        let (class, kind) = match self {
986            ExecutionError::ValidationFailed(_) => (ErrorClass::Integrity, "validation_failed"),
987            ExecutionError::Aborted => (ErrorClass::Abort, "aborted"),
988            ExecutionError::Io(_) => (ErrorClass::Integrity, "io"),
989            ExecutionError::StepFailed { .. } => (ErrorClass::Procedural, "step_failed"),
990            ExecutionError::UnknownAction(_) => (ErrorClass::Integrity, "unknown_action"),
991            ExecutionError::InvalidParams(_) => (ErrorClass::Integrity, "invalid_params"),
992            ExecutionError::EntropyError(_) => (ErrorClass::Integrity, "entropy_error"),
993            ExecutionError::MaterialLoadFailed { .. } => {
994                (ErrorClass::Integrity, "material_load_failed")
995            }
996            ExecutionError::OutputWriteFailed { .. } => {
997                (ErrorClass::Integrity, "output_write_failed")
998            }
999            ExecutionError::TranscriptError(_) => (ErrorClass::Integrity, "transcript_error"),
1000            ExecutionError::BackendError(e) => {
1001                let class = if e.retriability().is_retriable() {
1002                    ErrorClass::Environmental
1003                } else {
1004                    ErrorClass::Integrity
1005                };
1006                (class, "backend_error")
1007            }
1008        };
1009        ErrorRecord::new(class, kind, self.to_string())
1010    }
1011}
1012
1013fn validate_parameters(ceremony: &Ceremony) -> Result<(), ExecutionError> {
1014    let missing: Vec<_> = ceremony
1015        .parameters
1016        .iter()
1017        .filter(|(_, p)| p.value.is_null())
1018        .map(|(id, _)| id.as_str().to_string())
1019        .collect();
1020    if missing.is_empty() {
1021        Ok(())
1022    } else {
1023        Err(ExecutionError::ValidationFailed(format!(
1024            "Parameters missing values: {}",
1025            missing.join(", ")
1026        )))
1027    }
1028}
1029
1030/// Fold a step's produced artifacts into the running [`ExecutionState`].
1031fn merge_step_result(state: ExecutionState, result: StepResult) -> ExecutionState {
1032    let mut artifacts = state.artifacts;
1033    for (id, value) in result.artifacts {
1034        artifacts.insert(id, value);
1035    }
1036    ExecutionState {
1037        params: state.params,
1038        roles: state.roles,
1039        materials: state.materials,
1040        dry_run: state.dry_run,
1041        artifacts,
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use crossbeam_channel::unbounded;
1048    use rite_model::ActionType;
1049
1050    use super::*;
1051    use crate::transcript_sink::InMemorySink;
1052
1053    #[test]
1054    fn action_error_retriability_delegates_to_backend() {
1055        assert_eq!(
1056            ActionError::Backend(BackendError::TokenNotPresent).retriability(),
1057            Retriability::Transient,
1058        );
1059        assert_eq!(
1060            ActionError::Backend(BackendError::PinBlocked).retriability(),
1061            Retriability::Fatal,
1062        );
1063    }
1064
1065    #[test]
1066    fn handler_level_errors_are_fatal() {
1067        assert_eq!(ActionError::Aborted.retriability(), Retriability::Fatal);
1068        assert_eq!(
1069            ActionError::Disconnected.retriability(),
1070            Retriability::Fatal
1071        );
1072        assert_eq!(
1073            ActionError::Failed("check_value mismatch".into()).retriability(),
1074            Retriability::Fatal,
1075        );
1076    }
1077
1078    struct PingAction;
1079
1080    impl Action for PingAction {
1081        fn action_type(&self) -> ActionType {
1082            ActionType::Attest
1083        }
1084
1085        fn execute(
1086            &self,
1087            _step: &StepInfo,
1088            _ctx: &HandlerContext,
1089            _params: &serde_json::Value,
1090            reporter: &mut Reporter<'_>,
1091            _backend: Option<&mut dyn Backend>,
1092        ) -> Result<StepResult, ActionError> {
1093            reporter.log(crate::protocol::Icon::Info, "ping")?;
1094            Ok(StepResult::completed("pinged".to_string()))
1095        }
1096    }
1097
1098    fn minimal_ceremony() -> Ceremony {
1099        let yaml = r#"
1100version: "0.3"
1101name: "Test"
1102roles:
1103  participant:
1104    person: "Alice"
1105sections:
1106  main:
1107    role: ${role.participant}
1108    steps:
1109      ping:
1110        action: attest
1111        silent: true
1112        with:
1113          statement: "I confirm."
1114"#;
1115        rite_resolver::resolve(yaml, None)
1116            .into_result()
1117            .expect("resolve")
1118    }
1119
1120    /// Reports that this build has no puce, so a test can watch a
1121    /// build-relative finding travel through the registry.
1122    struct PickyAction;
1123
1124    impl Action for PickyAction {
1125        fn action_type(&self) -> ActionType {
1126            ActionType::Attest
1127        }
1128
1129        fn unsupported_params(&self, params: &serde_json::Value, _step: &StepInfo) -> Vec<String> {
1130            match params.get("statement").and_then(serde_json::Value::as_str) {
1131                Some("puce") => vec!["this build has no puce".to_string()],
1132                _ => Vec::new(),
1133            }
1134        }
1135
1136        fn execute(
1137            &self,
1138            _step: &StepInfo,
1139            _ctx: &HandlerContext,
1140            _params: &serde_json::Value,
1141            _reporter: &mut Reporter<'_>,
1142            _backend: Option<&mut dyn Backend>,
1143        ) -> Result<StepResult, ActionError> {
1144            Ok(StepResult::completed("done".to_string()))
1145        }
1146    }
1147
1148    fn ceremony_with_statement(statement: &str) -> Ceremony {
1149        let yaml = format!(
1150            r#"
1151version: "0.3"
1152name: "Test"
1153roles:
1154  participant:
1155    person: "Alice"
1156sections:
1157  main:
1158    role: ${{role.participant}}
1159    steps:
1160      ping:
1161        action: attest
1162        with:
1163          statement: "{statement}"
1164"#
1165        );
1166        rite_resolver::resolve(&yaml, None)
1167            .into_result()
1168            .expect("resolve")
1169    }
1170
1171    #[test]
1172    fn reports_what_this_build_cannot_carry_out() {
1173        let ceremony = ceremony_with_statement("puce");
1174        let mut registry = ActionRegistry::new();
1175        registry.register(Arc::new(PickyAction));
1176
1177        let found = registry.unsupported_step_params(&ceremony.execution_plan);
1178        let [issue] = found.as_slice() else {
1179            panic!("expected exactly one finding, got {found:?}");
1180        };
1181        assert_eq!(issue.step.as_str(), "ping");
1182        assert_eq!(issue.message, "this build has no puce");
1183    }
1184
1185    #[test]
1186    fn accepts_a_block_the_handler_is_happy_with() {
1187        let ceremony = ceremony_with_statement("I confirm.");
1188        let mut registry = ActionRegistry::new();
1189        registry.register(Arc::new(PickyAction));
1190
1191        assert!(
1192            registry
1193                .unsupported_step_params(&ceremony.execution_plan)
1194                .is_empty()
1195        );
1196    }
1197
1198    #[test]
1199    fn skips_an_action_this_build_has_no_handler_for() {
1200        // `unsupported_actions` is what reports these; asking a handler that
1201        // does not exist would be a second, misleading error on one step.
1202        let ceremony = ceremony_with_statement("puce");
1203        let registry = ActionRegistry::new();
1204
1205        assert!(
1206            registry
1207                .unsupported_step_params(&ceremony.execution_plan)
1208                .is_empty()
1209        );
1210    }
1211
1212    fn dry_run_output_config() -> OutputConfig {
1213        OutputConfig::new(std::path::PathBuf::from("/tmp/runner-test"))
1214    }
1215
1216    #[test]
1217    fn runs_a_minimal_ceremony_and_emits_lifecycle_facts() {
1218        let ceremony = minimal_ceremony();
1219        let mut registry = ActionRegistry::new();
1220        registry.register(Arc::new(PingAction));
1221
1222        let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1223        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1224        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1225
1226        let executor = Executor::new(
1227            ceremony,
1228            registry,
1229            BackendRegistry::new(),
1230            dry_run_output_config(),
1231            true,
1232            StartupSnapshot::placeholder(),
1233        );
1234
1235        let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
1236
1237        // Drain events. With silent=true and dry_run=true, no prompts fire.
1238        let mut facts = Vec::new();
1239        while let Ok(ev) = event_rx.recv() {
1240            if let ExecEvent::Fact { fact, .. } = ev {
1241                facts.push(fact);
1242            }
1243        }
1244
1245        let summary = join.join().expect("executor join").expect("run ok");
1246        assert_eq!(summary.steps_completed, 1);
1247
1248        let kinds: Vec<&'static str> = facts.iter().map(fact_kind).collect();
1249        assert_eq!(
1250            kinds,
1251            vec![
1252                "ceremony_started",
1253                "entropy_seeded",
1254                "role_declared",
1255                "role_assigned",
1256                "step_started",
1257                "step_completed",
1258                "ceremony_completed",
1259            ],
1260        );
1261
1262        drop(cmd_tx);
1263    }
1264
1265    /// Produces opened content, so a test can watch it reach the run
1266    /// directory through the same path as any other artifact.
1267    struct OpensContent;
1268
1269    impl Action for OpensContent {
1270        fn action_type(&self) -> ActionType {
1271            ActionType::Attest
1272        }
1273
1274        fn execute(
1275            &self,
1276            step: &StepInfo,
1277            _ctx: &HandlerContext,
1278            _params: &serde_json::Value,
1279            _reporter: &mut Reporter<'_>,
1280            _backend: Option<&mut dyn Backend>,
1281        ) -> Result<StepResult, ActionError> {
1282            let produces = step.produces.clone().expect("step creates");
1283            Ok(StepResult::completed_with_artifact(
1284                "opened",
1285                produces,
1286                crate::actions::ArtifactValue::Secret(secrecy::SecretBox::new(Box::new(
1287                    b"the recovery phrase".to_vec(),
1288                ))),
1289            ))
1290        }
1291    }
1292
1293    /// A ceremony whose one step creates `opened`, declared as an output.
1294    fn ceremony_writing_opened_content() -> Ceremony {
1295        let yaml = r#"
1296version: "0.3"
1297name: "Test"
1298roles:
1299  participant:
1300    person: "Alice"
1301output:
1302  opened:
1303    type: document
1304sections:
1305  main:
1306    role: ${role.participant}
1307    steps:
1308      open:
1309        action: attest
1310        silent: true
1311        with:
1312          statement: "I confirm."
1313        creates: opened
1314"#;
1315        rite_resolver::resolve(yaml, None)
1316            .into_result()
1317            .expect("resolve")
1318    }
1319
1320    /// Run `ceremony` to completion, acknowledging every prompt, and return
1321    /// the run's result with the facts it emitted.
1322    fn run_acknowledging_prompts(
1323        ceremony: Ceremony,
1324        output_config: OutputConfig,
1325    ) -> (Result<ExecutionSummary, ExecutionError>, Vec<StepFact>) {
1326        let mut registry = ActionRegistry::new();
1327        registry.register(Arc::new(OpensContent));
1328
1329        let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1330        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1331        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1332
1333        let executor = Executor::new(
1334            ceremony,
1335            registry,
1336            BackendRegistry::new(),
1337            output_config,
1338            false,
1339            StartupSnapshot::placeholder(),
1340        );
1341
1342        let frontend = std::thread::spawn({
1343            let cmd_tx = cmd_tx.clone();
1344            move || {
1345                let mut facts = Vec::new();
1346                while let Ok(event) = event_rx.recv() {
1347                    match event {
1348                        ExecEvent::Fact { fact, .. } => facts.push(fact),
1349                        ExecEvent::AwaitPrompt { prompt_id, .. } => {
1350                            let _ = cmd_tx.send(UiCommand::PromptResponse {
1351                                prompt_id,
1352                                response: crate::protocol::Response::Acknowledge,
1353                            });
1354                        }
1355                        _ => {}
1356                    }
1357                }
1358                facts
1359            }
1360        });
1361
1362        let result = executor.run(&cmd_rx, &event_tx, sink);
1363        drop(event_tx);
1364        drop(cmd_tx);
1365        let facts = frontend.join().expect("frontend join");
1366        (result, facts)
1367    }
1368
1369    #[test]
1370    fn opened_content_declared_as_an_output_is_written() {
1371        let dir = tempfile::tempdir().expect("tempdir");
1372        let config = OutputConfig::new(dir.path().to_path_buf());
1373        let (result, facts) = run_acknowledging_prompts(ceremony_writing_opened_content(), config);
1374
1375        let summary = result.expect("ceremony runs");
1376        assert_eq!(summary.steps_completed, 1);
1377        let written = facts.iter().find_map(|f| match f {
1378            StepFact::ArtifactWritten {
1379                name, file, digest, ..
1380            } => Some((name.clone(), file.clone(), digest.clone())),
1381            _ => None,
1382        });
1383        let (name, file, digest) = written.expect("the artifact is written");
1384        assert_eq!(name, "opened");
1385        // Opened content: no digest of a secret goes into the record.
1386        assert_eq!(digest, None);
1387        assert_eq!(
1388            std::fs::read(dir.path().join("artifacts").join(file)).expect("read artifact"),
1389            b"the recovery phrase"
1390        );
1391    }
1392
1393    #[test]
1394    fn run_stamps_every_fact_from_the_injected_clock() {
1395        use crate::test_support::{FixedClock, fixed_test_time};
1396
1397        let mut registry = ActionRegistry::new();
1398        registry.register(Arc::new(PingAction));
1399
1400        let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1401        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1402        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1403
1404        let fixed = fixed_test_time();
1405        let executor = Executor::new(
1406            minimal_ceremony(),
1407            registry,
1408            BackendRegistry::new(),
1409            dry_run_output_config(),
1410            true,
1411            StartupSnapshot::placeholder(),
1412        )
1413        .with_clock(Arc::new(FixedClock(fixed)));
1414
1415        let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
1416
1417        let mut times = Vec::new();
1418        while let Ok(ev) = event_rx.recv() {
1419            if let ExecEvent::Fact { at, .. } = ev {
1420                times.push(at);
1421            }
1422        }
1423        join.join().expect("executor join").expect("run ok");
1424
1425        // Every fact carries the injected time, including the terminal
1426        // `ceremony_completed`, which the runner emits outside `reporter.fact`.
1427        assert!(!times.is_empty());
1428        assert!(times.iter().all(|&t| t == fixed));
1429
1430        drop(cmd_tx);
1431    }
1432
1433    #[derive(Debug, PartialEq, Eq)]
1434    enum PacingTrace {
1435        CeremonyStarted,
1436        StepStarted(String),
1437        ContinuePrompt { step: Option<String> },
1438        StepCompleted,
1439        CeremonyCompleted,
1440    }
1441
1442    /// Two-step ceremony with pacing enabled: each step's body must be
1443    /// gated on a `Continue` prompt that the executor emits **between**
1444    /// `StepStarted` and the handler. After the last step there must be
1445    /// no trailing pause, only `StepCompleted` and `CeremonyCompleted`.
1446    #[test]
1447    #[allow(clippy::too_many_lines)]
1448    fn pacing_prompts_fire_at_step_start_not_step_end() {
1449        let yaml = r#"
1450version: "0.3"
1451name: "Pacing"
1452roles:
1453  participant:
1454    person: "Alice"
1455sections:
1456  main:
1457    role: ${role.participant}
1458    steps:
1459      first:
1460        action: attest
1461        with:
1462          statement: "first"
1463      second:
1464        action: attest
1465        with:
1466          statement: "second"
1467"#;
1468        let ceremony = rite_resolver::resolve(yaml, None)
1469            .into_result()
1470            .expect("resolve");
1471        let mut registry = ActionRegistry::new();
1472        registry.register(Arc::new(PingAction));
1473
1474        let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1475        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1476        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1477
1478        let executor = Executor::new(
1479            ceremony,
1480            registry,
1481            BackendRegistry::new(),
1482            dry_run_output_config(),
1483            false, // dry_run=false so prompts actually fire
1484            StartupSnapshot::placeholder(),
1485        );
1486
1487        // Drive the frontend on a worker so the executor's prompt waits
1488        // don't deadlock. Acknowledge every Continue that arrives and
1489        // collect a trace of the events the executor emitted.
1490        let frontend = std::thread::spawn({
1491            let cmd_tx = cmd_tx.clone();
1492            move || {
1493                let mut trace = Vec::new();
1494                while let Ok(event) = event_rx.recv() {
1495                    match event {
1496                        ExecEvent::Fact {
1497                            fact: StepFact::CeremonyStarted { .. },
1498                            ..
1499                        } => {
1500                            trace.push(PacingTrace::CeremonyStarted);
1501                        }
1502                        ExecEvent::Fact {
1503                            fact: StepFact::StepStarted { label, .. },
1504                            ..
1505                        } => {
1506                            trace.push(PacingTrace::StepStarted(label));
1507                        }
1508                        ExecEvent::Fact {
1509                            fact: StepFact::StepCompleted { .. },
1510                            ..
1511                        } => {
1512                            trace.push(PacingTrace::StepCompleted);
1513                        }
1514                        ExecEvent::Fact {
1515                            fact: StepFact::CeremonyCompleted { .. },
1516                            ..
1517                        } => {
1518                            trace.push(PacingTrace::CeremonyCompleted);
1519                        }
1520                        ExecEvent::AwaitPrompt {
1521                            prompt_id,
1522                            prompt: Prompt::Continue { .. },
1523                            step,
1524                            ..
1525                        } => {
1526                            trace.push(PacingTrace::ContinuePrompt {
1527                                step: step.map(|s| s.as_str().to_string()),
1528                            });
1529                            let _ = cmd_tx.send(UiCommand::PromptResponse {
1530                                prompt_id,
1531                                response: crate::protocol::Response::Acknowledge,
1532                            });
1533                        }
1534                        _ => {}
1535                    }
1536                }
1537                trace
1538            }
1539        });
1540
1541        let summary = executor
1542            .run(&cmd_rx, &event_tx, sink)
1543            .expect("ceremony runs");
1544        assert_eq!(summary.steps_completed, 2);
1545        // Drop the local senders so the frontend thread sees the
1546        // event channel close and returns.
1547        drop(event_tx);
1548        drop(cmd_tx);
1549        let trace = frontend.join().expect("frontend join");
1550
1551        // Required order:
1552        //   CeremonyStarted, Continue{None},
1553        //   StepStarted(1), Continue{Some(1)}, StepCompleted,
1554        //   StepStarted(2), Continue{Some(2)}, StepCompleted,
1555        //   CeremonyCompleted
1556        let mut iter = trace.iter();
1557        assert_eq!(iter.next(), Some(&PacingTrace::CeremonyStarted));
1558        assert_eq!(
1559            iter.next(),
1560            Some(&PacingTrace::ContinuePrompt { step: None })
1561        );
1562        let first_step = match iter.next() {
1563            Some(PacingTrace::StepStarted(label)) => label.clone(),
1564            other => panic!("expected first StepStarted, got {other:?}"),
1565        };
1566        assert_eq!(
1567            iter.next(),
1568            Some(&PacingTrace::ContinuePrompt {
1569                step: Some("first".to_string()),
1570            }),
1571            "first step's Continue prompt must fire after StepStarted and before the body",
1572        );
1573        assert_eq!(iter.next(), Some(&PacingTrace::StepCompleted));
1574        let second_step = match iter.next() {
1575            Some(PacingTrace::StepStarted(label)) => label.clone(),
1576            other => panic!("expected second StepStarted, got {other:?}"),
1577        };
1578        assert_eq!(
1579            iter.next(),
1580            Some(&PacingTrace::ContinuePrompt {
1581                step: Some("second".to_string()),
1582            }),
1583        );
1584        assert_eq!(iter.next(), Some(&PacingTrace::StepCompleted));
1585        assert_eq!(iter.next(), Some(&PacingTrace::CeremonyCompleted));
1586        assert_eq!(iter.next(), None, "no further events expected");
1587        // Steps are auto-numbered when there's only one section.
1588        assert_eq!(first_step, "1");
1589        assert_eq!(second_step, "2");
1590    }
1591
1592    #[test]
1593    fn aborts_when_action_returns_aborted() {
1594        struct AbortAction;
1595        impl Action for AbortAction {
1596            fn action_type(&self) -> ActionType {
1597                ActionType::Attest
1598            }
1599            fn execute(
1600                &self,
1601                _step: &StepInfo,
1602                _ctx: &HandlerContext,
1603                _params: &serde_json::Value,
1604                _reporter: &mut Reporter<'_>,
1605                _backend: Option<&mut dyn Backend>,
1606            ) -> Result<StepResult, ActionError> {
1607                Err(ActionError::Aborted)
1608            }
1609        }
1610
1611        let ceremony = minimal_ceremony();
1612        let mut registry = ActionRegistry::new();
1613        registry.register(Arc::new(AbortAction));
1614
1615        let (_cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1616        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1617        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1618
1619        let executor = Executor::new(
1620            ceremony,
1621            registry,
1622            BackendRegistry::new(),
1623            dry_run_output_config(),
1624            true,
1625            StartupSnapshot::placeholder(),
1626        );
1627
1628        let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
1629
1630        let mut got_failed = false;
1631        while let Ok(ev) = event_rx.recv() {
1632            if let ExecEvent::Fact {
1633                fact: StepFact::CeremonyFailed { error, .. },
1634                ..
1635            } = ev
1636            {
1637                assert_eq!(error.kind, "aborted");
1638                // An abort is classified distinctly from a failure, and its
1639                // message is ceremony-level (no empty step-id quotes).
1640                assert_eq!(error.class, ErrorClass::Abort);
1641                assert_eq!(error.message, "Ceremony aborted by the operator");
1642                got_failed = true;
1643            }
1644        }
1645        assert!(got_failed, "expected CeremonyFailed event");
1646
1647        let result = join.join().expect("executor join");
1648        assert!(matches!(result, Err(ExecutionError::Aborted)));
1649    }
1650
1651    // ---- Retry loop tests ----
1652
1653    /// What [`FlakyAction`] does at the start of every attempt (failing or
1654    /// not, mirroring an action that always prompts before its backend call),
1655    /// to exercise the re-executability gate.
1656    enum BeforeFail {
1657        Nothing,
1658        /// Emit a side-effect fact (a backend operation): closes the gate.
1659        SideEffect,
1660        /// Issue a secret prompt (like a PIN entry): the answered prompt is
1661        /// recorded but must not close the gate.
1662        Prompt,
1663        /// Record an operator deviation note: recorded but must not close
1664        /// the gate.
1665        Deviation,
1666    }
1667
1668    /// Test action whose first `fails_remaining` executions fail with
1669    /// `error()`, then it succeeds.
1670    struct FlakyAction {
1671        fails_remaining: std::sync::Mutex<u32>,
1672        error: fn() -> ActionError,
1673        before_fail: BeforeFail,
1674    }
1675
1676    impl FlakyAction {
1677        fn new(fails: u32, error: fn() -> ActionError, before_fail: BeforeFail) -> Arc<Self> {
1678            Arc::new(Self {
1679                fails_remaining: std::sync::Mutex::new(fails),
1680                error,
1681                before_fail,
1682            })
1683        }
1684    }
1685
1686    impl Action for FlakyAction {
1687        fn action_type(&self) -> ActionType {
1688            ActionType::Attest
1689        }
1690
1691        fn execute(
1692            &self,
1693            _step: &StepInfo,
1694            _ctx: &HandlerContext,
1695            _params: &serde_json::Value,
1696            reporter: &mut Reporter<'_>,
1697            _backend: Option<&mut dyn Backend>,
1698        ) -> Result<StepResult, ActionError> {
1699            let mut remaining = self.fails_remaining.lock().expect("lock");
1700            match self.before_fail {
1701                BeforeFail::Nothing => {}
1702                BeforeFail::SideEffect => {
1703                    reporter.backend_operation(
1704                        "test_op",
1705                        serde_json::Value::Null,
1706                        serde_json::Value::Null,
1707                        None,
1708                    )?;
1709                }
1710                BeforeFail::Prompt => {
1711                    reporter.prompt(&Prompt::Secret {
1712                        label: "Enter PIN".to_string(),
1713                        validator: rite_model::ValidatorSpec::NonEmpty,
1714                    })?;
1715                }
1716                BeforeFail::Deviation => {
1717                    reporter.fact(StepFact::DeviationRecorded {
1718                        step: None,
1719                        text: "operator note".to_string(),
1720                    })?;
1721                }
1722            }
1723            if *remaining == 0 {
1724                return Ok(StepResult::completed("done".to_string()));
1725            }
1726            *remaining = remaining.saturating_sub(1);
1727            Err((self.error)())
1728        }
1729    }
1730
1731    fn transient() -> ActionError {
1732        ActionError::Backend(BackendError::TokenNotPresent)
1733    }
1734
1735    fn ceremony_with_retry(retry_clause: &str) -> Ceremony {
1736        let yaml = format!(
1737            r#"
1738version: "0.3"
1739name: "Retry"
1740roles:
1741  participant:
1742    person: "Alice"
1743sections:
1744  main:
1745    role: ${{role.participant}}
1746    steps:
1747      work:
1748        action: attest
1749        silent: true
1750        {retry_clause}
1751        with:
1752          statement: "x"
1753"#
1754        );
1755        rite_resolver::resolve(&yaml, None)
1756            .into_result()
1757            .expect("resolve")
1758    }
1759
1760    /// Run `ceremony` with `action`, answering every retry `Confirm` with
1761    /// `confirm_answer`. Returns the run result, the emitted facts, and the
1762    /// number of retry prompts seen.
1763    fn drive(
1764        ceremony: Ceremony,
1765        action: Arc<dyn Action>,
1766        confirm_answer: bool,
1767    ) -> (
1768        Result<ExecutionSummary, ExecutionError>,
1769        Vec<StepFact>,
1770        usize,
1771    ) {
1772        drive_with_backends(ceremony, action, confirm_answer, BackendRegistry::new())
1773    }
1774
1775    /// Like [`drive`], with a caller-provided backend registry for steps
1776    /// that name a backend.
1777    fn drive_with_backends(
1778        ceremony: Ceremony,
1779        action: Arc<dyn Action>,
1780        confirm_answer: bool,
1781        backend_registry: BackendRegistry,
1782    ) -> (
1783        Result<ExecutionSummary, ExecutionError>,
1784        Vec<StepFact>,
1785        usize,
1786    ) {
1787        let (result, facts, confirms, _) =
1788            drive_run(ceremony, action, confirm_answer, backend_registry, true);
1789        (result, facts, confirms)
1790    }
1791
1792    /// Like [`drive_with_backends`], choosing whether the run is a dry run,
1793    /// which skips the pacing prompts. Each pacing prompt is acknowledged,
1794    /// and counted in the last value returned.
1795    fn drive_run(
1796        ceremony: Ceremony,
1797        action: Arc<dyn Action>,
1798        confirm_answer: bool,
1799        backend_registry: BackendRegistry,
1800        dry_run: bool,
1801    ) -> (
1802        Result<ExecutionSummary, ExecutionError>,
1803        Vec<StepFact>,
1804        usize,
1805        usize,
1806    ) {
1807        let mut registry = ActionRegistry::new();
1808        registry.register(action);
1809
1810        let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
1811        let (event_tx, event_rx) = unbounded::<ExecEvent>();
1812        let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
1813
1814        let executor = Executor::new(
1815            ceremony,
1816            registry,
1817            backend_registry,
1818            dry_run_output_config(),
1819            dry_run,
1820            StartupSnapshot::placeholder(),
1821        );
1822        let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
1823
1824        let mut facts = Vec::new();
1825        let mut confirms = 0usize;
1826        let mut continues = 0usize;
1827        while let Ok(ev) = event_rx.recv() {
1828            match ev {
1829                ExecEvent::Fact { fact, .. } => facts.push(fact),
1830                ExecEvent::AwaitPrompt {
1831                    prompt_id,
1832                    prompt: Prompt::Confirm { .. },
1833                    ..
1834                } => {
1835                    confirms = confirms.saturating_add(1);
1836                    cmd_tx
1837                        .send(UiCommand::PromptResponse {
1838                            prompt_id,
1839                            response: Response::Bool(confirm_answer),
1840                        })
1841                        .expect("send confirm");
1842                }
1843                ExecEvent::AwaitPrompt {
1844                    prompt_id,
1845                    prompt: Prompt::Secret { .. },
1846                    ..
1847                } => {
1848                    cmd_tx
1849                        .send(UiCommand::PromptResponse {
1850                            prompt_id,
1851                            response: Response::Secret("123456".to_string().into()),
1852                        })
1853                        .expect("send secret");
1854                }
1855                ExecEvent::AwaitPrompt {
1856                    prompt_id,
1857                    prompt: Prompt::Continue { .. },
1858                    ..
1859                } => {
1860                    continues = continues.saturating_add(1);
1861                    cmd_tx
1862                        .send(UiCommand::PromptResponse {
1863                            prompt_id,
1864                            response: Response::Acknowledge,
1865                        })
1866                        .expect("send acknowledge");
1867                }
1868                _ => {}
1869            }
1870        }
1871        let result = join.join().expect("executor join");
1872        (result, facts, confirms, continues)
1873    }
1874
1875    fn attempt_numbers(facts: &[StepFact]) -> Vec<u32> {
1876        facts
1877            .iter()
1878            .filter_map(|f| match f {
1879                StepFact::StepAttemptFailed { attempt, .. } => Some(*attempt),
1880                _ => None,
1881            })
1882            .collect()
1883    }
1884
1885    /// A value an expression produced is checked against the schema when the
1886    /// step runs, in the words a literal one gets at `rite check`, and the
1887    /// handler never sees it.
1888    #[test]
1889    fn an_evaluated_value_the_schema_refuses_fails_the_step_before_the_handler() {
1890        let yaml = r#"
1891version: "0.3"
1892name: "Evaluated"
1893roles:
1894  participant:
1895    person: "Alice"
1896parameters:
1897  n: { type: integer, default: 3 }
1898sections:
1899  main:
1900    role: ${role.participant}
1901    steps:
1902      work:
1903        action: attest
1904        silent: true
1905        with:
1906          statement: "${param.n}"
1907"#;
1908        let ceremony = rite_resolver::resolve(yaml, None)
1909            .into_result()
1910            .expect("an expression is deferred at resolution");
1911        // Would complete if it ran.
1912        let action = FlakyAction::new(0, transient, BeforeFail::Nothing);
1913        let (result, facts, confirms) = drive(ceremony, action, true);
1914
1915        assert!(result.is_err());
1916        assert_eq!(confirms, 0, "a value that is wrong stays wrong on retry");
1917        let error = facts
1918            .iter()
1919            .find_map(|f| match f {
1920                StepFact::StepAttemptFailed { error, .. } => Some(error.clone()),
1921                _ => None,
1922            })
1923            .expect("the attempt is recorded");
1924        // The value is evaluated, and could be a secret, so it is not shown.
1925        assert_eq!(
1926            error.message,
1927            "Invalid params: step 'work': 'with.statement' must be a string"
1928        );
1929        // The definition is at fault, not the operator.
1930        assert_eq!(error.class, ErrorClass::Integrity);
1931        assert_eq!(error.kind, "invalid_params");
1932        assert!(
1933            !facts
1934                .iter()
1935                .any(|f| matches!(f, StepFact::StepCompleted { .. }))
1936        );
1937    }
1938
1939    /// An `attest` whose defaults turn any block into a valid one.
1940    struct Defaulting;
1941
1942    impl Action for Defaulting {
1943        fn action_type(&self) -> ActionType {
1944            ActionType::Attest
1945        }
1946
1947        fn apply_defaults(&self, params: &mut serde_json::Value, _step: &StepInfo) {
1948            *params = serde_json::json!({ "statement": "the default" });
1949        }
1950
1951        fn execute(
1952            &self,
1953            _step: &StepInfo,
1954            _ctx: &HandlerContext,
1955            _params: &serde_json::Value,
1956            _reporter: &mut Reporter<'_>,
1957            _backend: Option<&mut dyn Backend>,
1958        ) -> Result<StepResult, ActionError> {
1959            Ok(StepResult::completed("done".to_string()))
1960        }
1961    }
1962
1963    /// The schema sees the block the definition wrote, not the one the
1964    /// handler's defaults make of it: a whole block an expression turned
1965    /// into a string is refused, as a literal string is at `rite check`.
1966    #[test]
1967    fn the_schema_checks_the_block_before_the_defaults() {
1968        let yaml = r#"
1969version: "0.3"
1970name: "Evaluated"
1971roles:
1972  participant:
1973    person: "Alice"
1974parameters:
1975  s: { type: string, default: "a statement" }
1976sections:
1977  main:
1978    role: ${role.participant}
1979    steps:
1980      work:
1981        action: attest
1982        silent: true
1983        with: "${param.s}"
1984"#;
1985        let ceremony = rite_resolver::resolve(yaml, None)
1986            .into_result()
1987            .expect("a whole block expression is deferred at resolution");
1988        let (result, facts, _) = drive(ceremony, Arc::new(Defaulting), true);
1989
1990        assert!(result.is_err());
1991        let error = facts
1992            .iter()
1993            .find_map(|f| match f {
1994                StepFact::StepAttemptFailed { error, .. } => Some(error.message.clone()),
1995                _ => None,
1996            })
1997            .expect("the attempt is recorded");
1998        assert_eq!(error, "Invalid params: step 'work': 'with' must be a map");
1999    }
2000
2001    /// A handler for `action` that completes, for runs that must stop before it.
2002    struct Completes(ActionType);
2003
2004    impl Action for Completes {
2005        fn action_type(&self) -> ActionType {
2006            self.0
2007        }
2008
2009        fn execute(
2010            &self,
2011            _step: &StepInfo,
2012            _ctx: &HandlerContext,
2013            _params: &serde_json::Value,
2014            _reporter: &mut Reporter<'_>,
2015            _backend: Option<&mut dyn Backend>,
2016        ) -> Result<StepResult, ActionError> {
2017            Ok(StepResult::completed("done".to_string()))
2018        }
2019    }
2020
2021    /// A rule over several values is checked when the step runs too, once an
2022    /// expression gave the value `rite check` could not know.
2023    #[test]
2024    fn a_rule_over_several_values_is_checked_when_the_step_runs() {
2025        let yaml = r#"
2026version: "0.3"
2027name: "Evaluated"
2028roles:
2029  participant:
2030    person: "Alice"
2031parameters:
2032  n: { type: integer, default: 8 }
2033sections:
2034  main:
2035    role: ${role.participant}
2036    steps:
2037      work:
2038        action: enter_secret
2039        silent: true
2040        with:
2041          message: "PIN"
2042          min_length: "${param.n}"
2043          max_length: 6
2044"#;
2045        let ceremony = rite_resolver::resolve(yaml, None)
2046            .into_result()
2047            .expect("the minimum is deferred at resolution");
2048        let (result, facts, _) =
2049            drive(ceremony, Arc::new(Completes(ActionType::EnterSecret)), true);
2050
2051        assert!(result.is_err());
2052        let error = facts
2053            .iter()
2054            .find_map(|f| match f {
2055                StepFact::StepAttemptFailed { error, .. } => Some(error.message.clone()),
2056                _ => None,
2057            })
2058            .expect("the attempt is recorded");
2059        assert_eq!(
2060            error,
2061            "Invalid params: step 'work': 'min_length' is above 'max_length', so no value fits"
2062        );
2063    }
2064
2065    /// A step whose params are refused fails before the operator is asked
2066    /// to start it, since starting it could not change them.
2067    #[test]
2068    fn a_refused_step_is_not_offered_to_the_operator() {
2069        let yaml = r#"
2070version: "0.3"
2071name: "Evaluated"
2072roles:
2073  participant:
2074    person: "Alice"
2075parameters:
2076  n: { type: integer, default: 3 }
2077sections:
2078  main:
2079    role: ${role.participant}
2080    steps:
2081      work:
2082        action: attest
2083        with:
2084          statement: "${param.n}"
2085"#;
2086        let ceremony = rite_resolver::resolve(yaml, None)
2087            .into_result()
2088            .expect("an expression is deferred at resolution");
2089        let (result, facts, _, continues) = drive_run(
2090            ceremony,
2091            Arc::new(Completes(ActionType::Attest)),
2092            true,
2093            BackendRegistry::new(),
2094            false,
2095        );
2096
2097        assert!(matches!(result, Err(ExecutionError::InvalidParams(_))));
2098        assert!(
2099            facts
2100                .iter()
2101                .any(|f| matches!(f, StepFact::StepAttemptFailed { .. }))
2102        );
2103        // Only the prompt that starts the ceremony.
2104        assert_eq!(continues, 1);
2105    }
2106
2107    /// A step without `with:` has no parameters, and the defaults fill them.
2108    #[test]
2109    fn a_step_without_with_runs_with_its_defaults() {
2110        let yaml = r#"
2111version: "0.3"
2112name: "Defaults"
2113roles:
2114  participant:
2115    person: "Alice"
2116sections:
2117  main:
2118    role: ${role.participant}
2119    steps:
2120      work:
2121        action: attest
2122        silent: true
2123"#;
2124        let ceremony = rite_resolver::resolve(yaml, None)
2125            .into_result()
2126            .expect("resolve");
2127        let (result, _, _) = drive(ceremony, Arc::new(Defaulting), true);
2128        assert_eq!(result.expect("run ok").steps_completed, 1);
2129    }
2130
2131    #[test]
2132    fn transient_failure_is_retried_then_succeeds() {
2133        let action = FlakyAction::new(1, transient, BeforeFail::Nothing);
2134        let (result, facts, confirms) = drive(ceremony_with_retry(""), action, true);
2135
2136        let summary = result.expect("run ok");
2137        assert_eq!(summary.steps_completed, 1);
2138        assert_eq!(confirms, 1, "one retry prompt");
2139        assert_eq!(attempt_numbers(&facts), vec![1]);
2140        // The recorded attempt is classified environmental (a transient backend error).
2141        let StepFact::StepAttemptFailed { error, .. } = facts
2142            .iter()
2143            .find(|f| matches!(f, StepFact::StepAttemptFailed { .. }))
2144            .expect("attempt fact")
2145        else {
2146            unreachable!()
2147        };
2148        assert_eq!(error.class, ErrorClass::Environmental);
2149        assert!(
2150            facts
2151                .iter()
2152                .any(|f| matches!(f, StepFact::StepCompleted { .. }))
2153        );
2154    }
2155
2156    #[test]
2157    fn retry_is_blocked_once_a_side_effect_was_emitted() {
2158        // Transient error, but the attempt already performed a backend
2159        // operation: the conservative gate refuses to retry, so no prompt
2160        // fires.
2161        let action = FlakyAction::new(u32::MAX, transient, BeforeFail::SideEffect);
2162        let (result, facts, confirms) = drive(ceremony_with_retry(""), action, true);
2163
2164        assert!(result.is_err(), "run fails");
2165        assert_eq!(confirms, 0, "gate blocks the retry prompt");
2166        assert_eq!(attempt_numbers(&facts), vec![1]);
2167        assert!(
2168            facts
2169                .iter()
2170                .any(|f| matches!(f, StepFact::CeremonyFailed { .. }))
2171        );
2172    }
2173
2174    #[test]
2175    fn prompting_before_a_transient_failure_stays_retriable() {
2176        // A PIN-style secret prompt answered before the failure records a
2177        // PromptAnswered fact, but answering a prompt is not a side effect:
2178        // the retry must still be offered, and the second attempt re-prompts.
2179        let action = FlakyAction::new(1, transient, BeforeFail::Prompt);
2180        let (result, facts, confirms) = drive(ceremony_with_retry(""), action, true);
2181
2182        let summary = result.expect("run ok");
2183        assert_eq!(summary.steps_completed, 1);
2184        assert_eq!(confirms, 1, "the retry prompt fired");
2185        assert_eq!(attempt_numbers(&facts), vec![1]);
2186        let secret_answers = facts
2187            .iter()
2188            .filter(|f| {
2189                matches!(
2190                    f,
2191                    StepFact::PromptAnswered {
2192                        prompt: Prompt::Secret { .. },
2193                        ..
2194                    }
2195                )
2196            })
2197            .count();
2198        assert_eq!(secret_answers, 2, "each attempt records its own answer");
2199    }
2200
2201    #[test]
2202    fn a_deviation_note_does_not_block_retry() {
2203        let action = FlakyAction::new(1, transient, BeforeFail::Deviation);
2204        let (result, _facts, confirms) = drive(ceremony_with_retry(""), action, true);
2205
2206        let summary = result.expect("run ok");
2207        assert_eq!(summary.steps_completed, 1);
2208        assert_eq!(confirms, 1, "the retry prompt fired");
2209    }
2210
2211    #[test]
2212    fn backend_acquisition_failure_is_retriable() {
2213        use std::sync::atomic::{AtomicU32, Ordering};
2214
2215        // Lazy backend initialization fails on the first probe (device
2216        // unplugged) and succeeds on the second: the operator must get the
2217        // same retry offer as for a transient error inside the handler.
2218        struct NullBackend {
2219            name: String,
2220        }
2221        impl Backend for NullBackend {
2222            fn name(&self) -> &str {
2223                &self.name
2224            }
2225            fn provider(&self) -> &'static str {
2226                "mock"
2227            }
2228            fn fingerprint(&self) -> String {
2229                format!("mock={}", self.name)
2230            }
2231        }
2232
2233        let probes = Arc::new(AtomicU32::new(0));
2234        let probes_in_factory = Arc::clone(&probes);
2235        let mut backends =
2236            crate::backend::BackendRegistry::with_factory(Box::new(move |_name, _config| {
2237                if probes_in_factory.fetch_add(1, Ordering::SeqCst) == 0 {
2238                    Err(BackendError::HardwareFailure("no device".to_string()))
2239                } else {
2240                    Ok(Box::new(NullBackend {
2241                        name: "token".to_string(),
2242                    }) as Box<dyn Backend>)
2243                }
2244            }));
2245        backends.declare(
2246            "token".to_string(),
2247            rite_sdk::BackendConfig {
2248                provider: "mock".to_string(),
2249                extra: serde_json::json!({}),
2250            },
2251        );
2252
2253        let yaml = r#"
2254version: "0.3"
2255name: "Retry"
2256backends:
2257  token:
2258    provider: mock
2259roles:
2260  participant:
2261    person: "Alice"
2262sections:
2263  main:
2264    role: ${role.participant}
2265    steps:
2266      work:
2267        action: attest
2268        backend: token
2269        silent: true
2270        with:
2271          statement: "x"
2272"#;
2273        let ceremony = rite_resolver::resolve(yaml, None)
2274            .into_result()
2275            .expect("resolve");
2276
2277        let action = FlakyAction::new(0, transient, BeforeFail::Nothing);
2278        let (result, facts, confirms) = drive_with_backends(ceremony, action, true, backends);
2279
2280        let summary = result.expect("run ok after the operator retried");
2281        assert_eq!(summary.steps_completed, 1);
2282        assert_eq!(confirms, 1, "the retry prompt fired");
2283        assert_eq!(attempt_numbers(&facts), vec![1]);
2284        let StepFact::StepAttemptFailed { error, .. } = facts
2285            .iter()
2286            .find(|f| matches!(f, StepFact::StepAttemptFailed { .. }))
2287            .expect("attempt fact")
2288        else {
2289            unreachable!()
2290        };
2291        assert_eq!(error.class, ErrorClass::Environmental);
2292        assert_eq!(probes.load(Ordering::SeqCst), 2, "hardware probed again");
2293    }
2294
2295    #[test]
2296    fn retry_never_fails_immediately() {
2297        let action = FlakyAction::new(u32::MAX, transient, BeforeFail::Nothing);
2298        let (result, facts, confirms) = drive(ceremony_with_retry("retry: never"), action, true);
2299
2300        assert!(result.is_err());
2301        assert_eq!(confirms, 0, "retry: never never prompts");
2302        assert_eq!(attempt_numbers(&facts), vec![1]);
2303    }
2304
2305    #[test]
2306    fn retry_attempts_caps_total_tries() {
2307        let action = FlakyAction::new(u32::MAX, transient, BeforeFail::Nothing);
2308        let (result, facts, confirms) =
2309            drive(ceremony_with_retry("retry: {attempts: 2}"), action, true);
2310
2311        assert!(result.is_err());
2312        // Attempt 1 fails and prompts; attempt 2 fails and the cap forbids a
2313        // further prompt.
2314        assert_eq!(confirms, 1);
2315        assert_eq!(attempt_numbers(&facts), vec![1, 2]);
2316    }
2317
2318    #[test]
2319    fn declining_the_retry_prompt_terminates() {
2320        let action = FlakyAction::new(u32::MAX, transient, BeforeFail::Nothing);
2321        let (result, facts, confirms) = drive(ceremony_with_retry(""), action, false);
2322
2323        assert!(result.is_err());
2324        assert_eq!(confirms, 1, "prompted once, operator declined");
2325        assert_eq!(attempt_numbers(&facts), vec![1]);
2326    }
2327
2328    #[test]
2329    fn artifact_levels_follow_what_the_artifact_is() {
2330        use secrecy::SecretBox;
2331
2332        let bytes = || ArtifactValue::Bytes(b"x".to_vec());
2333        assert_eq!(
2334            artifact_level(ActionType::SignData, &bytes()),
2335            Level::PUBLIC
2336        );
2337        assert_eq!(
2338            artifact_level(ActionType::GenerateCsr, &bytes()),
2339            Level::PUBLIC
2340        );
2341        // Bytes from any other step may be a document the ceremony handled.
2342        assert_eq!(
2343            artifact_level(ActionType::DecryptData, &bytes()),
2344            Level::RESTRICTED
2345        );
2346        assert_eq!(
2347            artifact_level(
2348                ActionType::DecryptData,
2349                &ArtifactValue::Secret(SecretBox::new(Box::new(b"x".to_vec())))
2350            ),
2351            Level::CONFIDENTIAL
2352        );
2353    }
2354
2355    fn fact_kind(fact: &StepFact) -> &'static str {
2356        match fact {
2357            StepFact::CeremonyStarted { .. } => "ceremony_started",
2358            StepFact::ActStarted { .. } => "act_started",
2359            StepFact::StepStarted { .. } => "step_started",
2360            StepFact::PromptAnswered { .. } => "prompt_answered",
2361            StepFact::BackendOperation { .. } => "backend_operation",
2362            StepFact::AttestationRecorded { .. } => "attestation_recorded",
2363            StepFact::ArtifactWritten { .. } => "artifact_written",
2364            StepFact::DeviationRecorded { .. } => "deviation_recorded",
2365            StepFact::StepAttemptFailed { .. } => "step_attempt_failed",
2366            StepFact::StepCompleted { .. } => "step_completed",
2367            StepFact::CeremonyCompleted { .. } => "ceremony_completed",
2368            StepFact::CeremonyFailed { .. } => "ceremony_failed",
2369            StepFact::EntropySeeded { .. } => "entropy_seeded",
2370            StepFact::EntropyContributed { .. } => "entropy_contributed",
2371            StepFact::EntropyDrawn { .. } => "entropy_drawn",
2372            StepFact::RoleDeclared { .. } => "role_declared",
2373            StepFact::RoleAssigned { .. } => "role_assigned",
2374            StepFact::ParameterBound { .. } => "parameter_bound",
2375            StepFact::MaterialLoaded { .. } => "material_loaded",
2376            StepFact::MaterialDigest { .. } => "material_digest",
2377            StepFact::BackendBound { .. } => "backend_bound",
2378            StepFact::MachineInfoRecorded { .. } => "machine_info_recorded",
2379            _ => "unknown",
2380        }
2381    }
2382}