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