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}