1use 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
63fn 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
83pub const PRODUCER: &str = concat!("rite ", env!("CARGO_PKG_VERSION"));
86
87fn 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#[derive(Debug, Error)]
96pub enum ActionError {
97 #[error("ceremony aborted by operator")]
100 Aborted,
101 #[error("frontend channel disconnected")]
103 Disconnected,
104 #[error("transcript write failed: {0}")]
106 Transcript(std::io::Error),
107 #[error("backend error: {0}")]
109 Backend(#[from] BackendError),
110 #[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 #[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
151pub 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
182pub trait Action: Send + Sync {
189 fn action_type(&self) -> ActionType;
191
192 fn apply_defaults(&self, _params: &mut serde_json::Value, _step: &StepInfo) {}
195
196 fn unsupported_params(&self, _params: &serde_json::Value, _step: &StepInfo) -> Vec<String> {
211 Vec::new()
212 }
213
214 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#[derive(Debug, Clone)]
238pub struct StepUnsupportedParam {
239 pub step: rite_model::StepId,
241 pub message: String,
243}
244
245#[derive(Default)]
247pub struct ActionRegistry {
248 actions: HashMap<ActionType, Arc<dyn Action>>,
249}
250
251impl ActionRegistry {
252 #[must_use]
254 pub fn new() -> Self {
255 Self::default()
256 }
257
258 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 #[must_use]
266 pub fn get(&self, action: &ActionType) -> Option<&Arc<dyn Action>> {
267 self.actions.get(action)
268 }
269
270 pub fn action_types(&self) -> impl Iterator<Item = &ActionType> {
272 self.actions.keys()
273 }
274
275 #[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 #[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(¶ms, &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#[derive(Debug)]
326pub struct ExecutionSummary {
327 pub ceremony_name: String,
329 pub steps_completed: usize,
331 pub transcript_fingerprint: TranscriptFingerprint,
333}
334
335pub 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 #[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 #[must_use]
375 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
376 self.clock = clock;
377 self
378 }
379
380 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 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 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 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 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 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 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 reporter.signal(UiSignal::SystemInfo(Box::new(self.startup.system.clone())))?;
554 reporter.signal(UiSignal::Environment(self.startup.environment.clone()))?;
555
556 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 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 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 let plan = std::mem::take(&mut self.ceremony.execution_plan);
609 for step in &plan {
610 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 let handler = self
643 .registry
644 .get(&step.action)
645 .ok_or(ExecutionError::UnknownAction(step.action))?
646 .clone();
647
648 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 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 ¶ms,
673 reporter,
674 &mut backends,
675 )?;
676
677 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 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 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 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
759fn 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
789struct RunBackends<'a> {
791 registry: &'a mut BackendRegistry,
792 bound: HashSet<String>,
793}
794
795impl RunBackends<'_> {
796 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
816fn 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
852fn 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 if params.is_null() {
869 params = serde_json::Value::Object(serde_json::Map::new());
870 }
871 if let Err(reason) = check_params(step.action, ¶ms) {
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
884fn 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 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 Err(ActionError::Aborted) => return Err(ExecutionError::Aborted),
921 Err(action_err) => action_err,
922 };
923
924 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 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
958fn 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 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
1030fn 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 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 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 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 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 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 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 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 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 #[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, StartupSnapshot::placeholder(),
1485 );
1486
1487 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(event_tx);
1548 drop(cmd_tx);
1549 let trace = frontend.join().expect("frontend join");
1550
1551 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 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 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 enum BeforeFail {
1657 Nothing,
1658 SideEffect,
1660 Prompt,
1663 Deviation,
1666 }
1667
1668 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 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 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 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 #[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 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 assert_eq!(
1926 error.message,
1927 "Invalid params: step 'work': 'with.statement' must be a string"
1928 );
1929 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 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 #[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 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 #[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 #[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 assert_eq!(continues, 1);
2105 }
2106
2107 #[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 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 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 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 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 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 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}