Skip to main content

rite_runtime/
protocol.rs

1//! Channel vocabulary between the executor and a frontend.
2//!
3//! The persisted transcript schema (`StepFact` + friends) lives in
4//! [`rite_model::transcript`] so non-executor consumers (`rite-render`,
5//! third-party verifiers) can parse a transcript without depending on
6//! the runtime. This module owns only the **in-flight** vocabulary ,
7//! channel events and commands, prompt response values that carry
8//! plaintext, and UI-only signals.
9//!
10//! - [`ExecEvent`], runtime → frontend (`Fact`, `Signal`, `AwaitPrompt`)
11//! - [`UiCommand`], frontend → runtime
12//! - [`Response`], in-flight user response (channel-only, never serialized;
13//!   converted to [`rite_model::ResponseRecord`] when persisted)
14//! - [`UiSignal`], [`Icon`], UI narration, never on disk
15
16use rite_model::{Material, MaterialId, MaterialKind, Prompt, StepFact, StepId};
17use secrecy::SecretString;
18
19use crate::system_info::{Environment, SystemInfo};
20
21/// Identifier that pairs a prompt request with its matching response.
22///
23/// Issued by the runtime when emitting [`ExecEvent::AwaitPrompt`]; echoed back
24/// by the frontend in [`UiCommand::PromptResponse`]. Allows the runtime to
25/// reject stale or out-of-order responses unambiguously.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct PromptId(u64);
28
29impl PromptId {
30    /// Create a new identifier. Callers (the executor) ensure uniqueness
31    /// within a ceremony run.
32    #[must_use]
33    pub fn new(value: u64) -> Self {
34        Self(value)
35    }
36
37    /// Underlying numeric value.
38    #[must_use]
39    pub fn value(self) -> u64 {
40        self.0
41    }
42}
43
44/// Visual cue for UI lines. Mapped to a glyph by the frontend.
45///
46/// The runtime intentionally does not provide a `Display` impl: glyphs are a
47/// presentation concern owned by the frontend (the TUI animates `Spinner` over
48/// multiple frames, the console picks a single character per icon, a future
49/// log-only frontend may use ASCII).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Icon {
52    /// Operation in progress; animated in TUI.
53    Spinner,
54    /// Success / completion.
55    Checkmark,
56    /// Failure / error.
57    Cross,
58    /// Informational message.
59    Info,
60    /// Warning (non-fatal).
61    Warning,
62}
63
64/// In-process user response.
65///
66/// Carries plaintext secrets and is intentionally **not** `Clone` or
67/// `Serialize`, both would risk accidental leakage. For transcript
68/// serialization use [`rite_model::ResponseRecord`], which encodes secrets
69/// as a redaction marker with a hash of the plaintext.
70#[derive(Debug)]
71pub enum Response {
72    /// Yes / no answer.
73    Bool(bool),
74    /// Free-form text answer (already validated by the runtime).
75    Text(String),
76    /// Sensitive answer. Wrapped in [`SecretString`] to zeroize on drop.
77    Secret(SecretString),
78    /// Acknowledgement of a [`Prompt::Continue`].
79    Acknowledge,
80}
81
82/// Overview of a declared material, carried by
83/// [`UiSignal::CeremonyOverview`] so the live UI can render the material
84/// list. In-flight only: the ceremony YAML is the source of truth for
85/// material metadata, so this never reaches the transcript.
86#[derive(Debug, Clone)]
87pub struct MaterialOverview {
88    /// Material identifier.
89    pub id: MaterialId,
90    /// Optional title; falls back to `id` for display.
91    pub title: Option<String>,
92    /// Optional human-readable description.
93    pub description: Option<String>,
94    /// Kind of material (digital file or physical item).
95    pub kind: MaterialOverviewKind,
96}
97
98/// Kind of material in [`MaterialOverview`]. Mirrors the structural
99/// distinction of the IR's [`MaterialKind`] without the load-time
100/// implementation detail (file sources).
101#[non_exhaustive]
102#[derive(Debug, Clone)]
103pub enum MaterialOverviewKind {
104    /// File-backed digital material.
105    Digital,
106    /// Tangible item with optional identifier and quantity.
107    Physical {
108        /// Human-readable identifier (e.g. serial number).
109        identifier: Option<String>,
110        /// Item count for checklist rendering.
111        quantity: Option<u32>,
112    },
113}
114
115impl MaterialOverviewKind {
116    /// Construct a [`MaterialOverviewKind::Physical`] with optional
117    /// identifier and quantity. Needed because the variant is reachable
118    /// only via the constructor when the enum stays `#[non_exhaustive]`.
119    #[must_use]
120    pub fn physical(identifier: Option<String>, quantity: Option<u32>) -> Self {
121        Self::Physical {
122            identifier,
123            quantity,
124        }
125    }
126}
127
128impl MaterialOverview {
129    /// Display name: the optional title, falling back to the material id.
130    /// Mirrors [`Material::display_name`] for the channel-side shape.
131    #[must_use]
132    pub fn display_title(&self) -> &str {
133        self.title.as_deref().unwrap_or(self.id.as_str())
134    }
135
136    /// Project a resolved IR [`Material`] onto the overview shape sent to
137    /// frontends.
138    #[must_use]
139    pub fn from_material(material: &Material) -> Self {
140        let kind = match &material.kind {
141            MaterialKind::Digital { .. } => MaterialOverviewKind::Digital,
142            MaterialKind::Physical {
143                identifier,
144                quantity,
145            } => MaterialOverviewKind::Physical {
146                identifier: identifier.clone(),
147                quantity: *quantity,
148            },
149        };
150        Self {
151            id: material.id.clone(),
152            title: material.title.clone(),
153            description: material.description.clone(),
154            kind,
155        }
156    }
157}
158
159/// Transient UI-only signal. Never written to the transcript.
160///
161/// Signals are operator-assistance, not evidence. A frontend may **drop any
162/// variant** without affecting correctness or the transcript (this is exactly
163/// why `headless` and `console` ignore most of them). To persist information,
164/// emit a [`StepFact`] from an action; nothing here is ever promoted into the
165/// transcript. See `docs/development/runtime-and-frontend.md` for the full
166/// fact-vs-signal model.
167///
168/// The enum is intentionally **not** `#[non_exhaustive]`: it is a
169/// workspace-internal protocol type, and forcing every frontend to match each
170/// variant exhaustively (no wildcard arms) is the wanted compile-time check
171/// that a new signal is consciously handled or ignored everywhere. Public-API
172/// enums still get `#[non_exhaustive]`.
173///
174/// Variants fall into three sub-families:
175/// - **narration**: ephemeral lines and progress ([`LogLine`](Self::LogLine),
176///   [`Progress`](Self::Progress)).
177/// - **one-shot structured**: a single snapshot emitted at ceremony start
178///   ([`CeremonyOverview`](Self::CeremonyOverview),
179///   [`SystemInfo`](Self::SystemInfo)).
180/// - **re-emittable structured**: state the runtime may resend during the run;
181///   the frontend replaces its view on each ([`Environment`](Self::Environment)).
182#[derive(Debug, Clone)]
183pub enum UiSignal {
184    // --- narration ---
185    /// Human-readable narration line for the live UI.
186    LogLine {
187        /// Step the line belongs to (if any).
188        step: Option<StepId>,
189        /// Visual cue.
190        icon: Icon,
191        /// Line text.
192        text: String,
193    },
194    /// Progress signal for spinners and progress bars.
195    Progress {
196        /// Step the progress belongs to.
197        step: StepId,
198        /// Short phase label (e.g. `signing`, `verifying`).
199        phase: String,
200        /// Optional completion fraction in `[0.0, 1.0]`.
201        fraction: Option<f32>,
202    },
203
204    // --- one-shot structured ---
205    /// Descriptive metadata for the pre-ceremony overview screen. Emitted
206    /// once, right after [`StepFact::CeremonyStarted`]. Deliberately kept
207    /// out of the transcript: the YAML is the source of truth for these
208    /// fields, and embedding them in the persisted record would bloat
209    /// every transcript with data that the verifier could pull from the
210    /// ceremony source instead.
211    CeremonyOverview {
212        /// Optional ceremony description.
213        description: Option<String>,
214        /// Declared materials, in declaration order.
215        materials: Vec<MaterialOverview>,
216        /// Total number of steps in the execution plan.
217        step_count: usize,
218    },
219    /// Static build and host identity for the System tab. Emitted once, right
220    /// after [`CeremonyOverview`](Self::CeremonyOverview). UI-only: machine
221    /// identity that belongs in the transcript is recorded by the
222    /// `machine_info` action, not fed in through this channel.
223    ///
224    /// Boxed: [`SystemInfo`] is much larger than the other variants, and
225    /// keeping the enum (and `ExecEvent`) small avoids bloating every
226    /// channel message.
227    SystemInfo(Box<SystemInfo>),
228
229    // --- re-emittable structured ---
230    /// Live device inventory for the System tab. Emitted once today, but
231    /// shaped to be **re-emitted**: the frontend replaces its environment view
232    /// on each, so a future live observer is purely additive. UI-only, never
233    /// persisted.
234    Environment(Environment),
235}
236
237/// Event emitted by the runtime to the frontend.
238#[derive(Debug)]
239pub enum ExecEvent {
240    /// A transcript-worthy fact. Already persisted by the transcript sink
241    /// by the time it reaches the frontend.
242    Fact(StepFact),
243    /// A UI-only signal.
244    Signal(UiSignal),
245    /// The runtime is waiting for a user response. The frontend must reply
246    /// with [`UiCommand::PromptResponse`] carrying the matching `prompt_id`.
247    AwaitPrompt {
248        /// Step that issued the prompt, if any. `None` for ceremony-level
249        /// prompts (e.g. the ceremony-start confirmation emitted before
250        /// the first step).
251        step: Option<StepId>,
252        /// Identifier echoed back in the matching response.
253        prompt_id: PromptId,
254        /// The prompt itself.
255        prompt: Prompt,
256        /// If a previous attempt was rejected by the runtime's validator,
257        /// the rejection reason for the frontend to surface to the user.
258        previous_attempt_rejected_because: Option<String>,
259    },
260    /// Sent once after the terminal fact has been recorded and the chain
261    /// head is final. Out-of-band so the fact itself doesn't carry the
262    /// hash of the chain it belongs to. Frontends use this to display the
263    /// fingerprint on the completion screen.
264    Finalized {
265        /// `sha256:<hex>` of the last line in the transcript.
266        fingerprint: String,
267    },
268}
269
270/// Command sent by the frontend to the runtime.
271#[derive(Debug)]
272pub enum UiCommand {
273    /// Response to an [`ExecEvent::AwaitPrompt`].
274    PromptResponse {
275        /// Identifier of the prompt being answered.
276        prompt_id: PromptId,
277        /// The response.
278        response: Response,
279    },
280    /// Record a deviation. Available at any time.
281    LogDeviation {
282        /// Verbatim deviation text.
283        text: String,
284    },
285    /// Abort the ceremony at the next safe point.
286    Abort,
287}