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}