Skip to main content

meerkat_runtime/store/
mod.rs

1//! RuntimeStore — atomic persistence for runtime state.
2//!
3//! Machine-owned runtime commands durably persist [`RunBoundaryReceipt`] values
4//! atomically with their session and input-state effects.
5
6pub mod memory;
7#[cfg(feature = "sqlite-store")]
8pub mod sqlite;
9
10use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
11
12use crate::identifiers::LogicalRuntimeId;
13use crate::input_state::{InputStatePersistenceRecord, StoredInputState};
14use crate::runtime_state::RuntimeState;
15
16const LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 1;
17const SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 2;
18const MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 3;
19
20/// Errors from RuntimeStore operations.
21#[derive(Debug, Clone, thiserror::Error)]
22#[non_exhaustive]
23pub enum RuntimeStoreError {
24    /// Write failed.
25    #[error("Store write failed: {0}")]
26    WriteFailed(String),
27    /// Read failed.
28    #[error("Store read failed: {0}")]
29    ReadFailed(String),
30    /// The explicit session-store key does not match the serialized session.
31    #[error("Session store key mismatch: expected {expected}, actual {actual}")]
32    SessionKeyMismatch {
33        expected: meerkat_core::types::SessionId,
34        actual: meerkat_core::types::SessionId,
35    },
36    /// Not found.
37    #[error("Not found: {0}")]
38    NotFound(String),
39    /// Operation is not supported by this store implementation.
40    #[error("Unsupported store operation: {0}")]
41    Unsupported(String),
42    /// A detached producer attempted to persist an ops snapshot after the
43    /// matching epoch was atomically retired by unregister.
44    #[error("Ops lifecycle epoch {epoch_id} for runtime {runtime_id} is retired")]
45    OpsLifecycleEpochRetired {
46        runtime_id: String,
47        epoch_id: meerkat_core::RuntimeEpochId,
48    },
49    /// An unregister-finalization commit may have become durable, but the
50    /// backend could not authoritatively classify its outcome.
51    ///
52    /// Callers must retry the idempotent atomic finalization and must not
53    /// publish a compensating lifecycle rollback for this error.
54    #[error("Unregister finalization outcome is unknown: {0}")]
55    UnregisterFinalizationOutcomeUnknown(String),
56    /// Runtime snapshot CAS rejected a stale transcript rewrite.
57    #[error("Transcript revision conflict: expected {expected}, actual {actual}")]
58    TranscriptRevisionConflict { expected: String, actual: String },
59    /// Internal error.
60    #[error("Internal error: {0}")]
61    Internal(String),
62}
63
64/// Transactional updater for the runtime-owned OAuth login-flow payload snapshot.
65pub type AuthOAuthFlowSnapshotUpdate<'a> =
66    dyn FnMut(Option<&[u8]>) -> Result<Vec<u8>, RuntimeStoreError> + 'a;
67
68/// Describes a serialized session snapshot for boundary and snapshot-only commits.
69#[derive(Debug, Clone)]
70pub struct SessionDelta {
71    /// Serialized session snapshot (opaque to RuntimeStore).
72    pub session_snapshot: Vec<u8>,
73}
74
75fn validated_compaction_projection_intents(
76    session: &meerkat_core::Session,
77) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
78    session
79        .validated_compaction_projection_intents()
80        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))
81}
82
83/// Runtime binding facts selected by generated MeerkatMachine authority.
84///
85/// RuntimeStore implementations persist and read these facts as part of a
86/// machine lifecycle snapshot. The commit token that writes these facts stays
87/// crate-private so compatibility callers cannot mint replacement lifecycle
88/// truth.
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
90pub struct MachineLifecycleBindingFacts {
91    agent_runtime_id: Option<String>,
92    fence_token: Option<u64>,
93    runtime_generation: Option<u64>,
94    runtime_epoch_id: Option<String>,
95}
96
97/// Durable identity receipt for the last completed supervisor revoke.
98///
99/// This is not a live supervisor binding and carries no route address. It is
100/// only the identity/key/epoch witness needed to authorize an exact duplicate
101/// revoke response after a cold restart; the current authenticated request
102/// supplies its current route.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct RevokedSupervisorReceipt {
105    peer_id: String,
106    signing_public_key: String,
107    epoch: u64,
108}
109
110/// Durable current supervisor binding used to authenticate terminal retry
111/// traffic after a cold runtime restart.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct SupervisorBindingReceipt {
114    name: String,
115    peer_id: String,
116    address: String,
117    signing_public_key: String,
118    epoch: u64,
119}
120
121/// Durable in-flight supervisor revocation receipt.
122///
123/// This is the closed-world hand-off between generated machine authority and
124/// the concrete router mutation.  It deliberately retains the complete prior
125/// route so a cold runtime can authenticate an exact retry and re-materialize
126/// the generated remove obligation without resurrecting a live binding.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SupervisorRevocationPendingReceipt {
129    name: String,
130    peer_id: String,
131    address: String,
132    signing_public_key: String,
133    epoch: u64,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum SupervisorRotationPersistencePhase {
139    PreviousRevokePending,
140    NextPublishPending,
141    Completed,
142    Rejected,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum SupervisorRotationRejection {
148    OperationConflict,
149    NotBound,
150    SenderMismatch,
151    TargetEpochNotAdvanced,
152    InvalidTarget,
153    UnsupportedProtocolVersion,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct SupervisorRotationReceipt {
158    operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
159    phase: SupervisorRotationPersistencePhase,
160    rejection: Option<SupervisorRotationRejection>,
161    previous: SupervisorBindingReceipt,
162    next: SupervisorBindingReceipt,
163}
164
165impl SupervisorRotationReceipt {
166    pub(crate) fn new(
167        operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
168        phase: SupervisorRotationPersistencePhase,
169        rejection: Option<SupervisorRotationRejection>,
170        previous: SupervisorBindingReceipt,
171        next: SupervisorBindingReceipt,
172    ) -> Self {
173        Self {
174            operation_id,
175            phase,
176            rejection,
177            previous,
178            next,
179        }
180    }
181
182    pub fn operation_id(
183        &self,
184    ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
185        self.operation_id
186    }
187
188    pub fn phase(&self) -> SupervisorRotationPersistencePhase {
189        self.phase
190    }
191
192    pub fn rejection(&self) -> Option<SupervisorRotationRejection> {
193        self.rejection
194    }
195
196    pub fn previous(&self) -> &SupervisorBindingReceipt {
197        &self.previous
198    }
199
200    pub fn next(&self) -> &SupervisorBindingReceipt {
201        &self.next
202    }
203}
204
205impl SupervisorBindingReceipt {
206    pub(crate) fn new(
207        name: String,
208        peer_id: String,
209        address: String,
210        signing_public_key: String,
211        epoch: u64,
212    ) -> Self {
213        Self {
214            name,
215            peer_id,
216            address,
217            signing_public_key,
218            epoch,
219        }
220    }
221
222    pub fn name(&self) -> &str {
223        &self.name
224    }
225
226    pub fn peer_id(&self) -> &str {
227        &self.peer_id
228    }
229
230    pub fn address(&self) -> &str {
231        &self.address
232    }
233
234    pub fn signing_public_key(&self) -> &str {
235        &self.signing_public_key
236    }
237
238    pub fn epoch(&self) -> u64 {
239        self.epoch
240    }
241}
242
243impl RevokedSupervisorReceipt {
244    pub(crate) fn new(peer_id: String, signing_public_key: String, epoch: u64) -> Self {
245        Self {
246            peer_id,
247            signing_public_key,
248            epoch,
249        }
250    }
251
252    pub fn peer_id(&self) -> &str {
253        &self.peer_id
254    }
255
256    pub fn signing_public_key(&self) -> &str {
257        &self.signing_public_key
258    }
259
260    pub fn epoch(&self) -> u64 {
261        self.epoch
262    }
263}
264
265impl SupervisorRevocationPendingReceipt {
266    pub(crate) fn new(
267        name: String,
268        peer_id: String,
269        address: String,
270        signing_public_key: String,
271        epoch: u64,
272    ) -> Self {
273        Self {
274            name,
275            peer_id,
276            address,
277            signing_public_key,
278            epoch,
279        }
280    }
281
282    pub fn name(&self) -> &str {
283        &self.name
284    }
285
286    pub fn peer_id(&self) -> &str {
287        &self.peer_id
288    }
289
290    pub fn address(&self) -> &str {
291        &self.address
292    }
293
294    pub fn signing_public_key(&self) -> &str {
295        &self.signing_public_key
296    }
297
298    pub fn epoch(&self) -> u64 {
299        self.epoch
300    }
301}
302
303/// Closed durable supervisor authority state. Each variant owns one complete
304/// recovery shape; terminal rotation receipts retain their exact operation and
305/// participant descriptors for idempotent submission and later observation.
306#[derive(Debug, Clone, Default, PartialEq, Eq)]
307pub enum SupervisorAuthoritySnapshot {
308    #[default]
309    UnboundNoReceipt,
310    Bound(SupervisorBindingReceipt),
311    RevocationPending(SupervisorRevocationPendingReceipt),
312    RotationOperation(SupervisorRotationReceipt),
313    RevokedReceipt(RevokedSupervisorReceipt),
314    WithRotationHistory {
315        current: Box<SupervisorAuthoritySnapshot>,
316        terminal_receipts: std::collections::BTreeMap<
317            meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
318            SupervisorRotationReceipt,
319        >,
320    },
321}
322
323impl MachineLifecycleBindingFacts {
324    pub(crate) fn new(
325        agent_runtime_id: Option<String>,
326        fence_token: Option<u64>,
327        runtime_generation: Option<u64>,
328        runtime_epoch_id: Option<String>,
329    ) -> Self {
330        Self {
331            agent_runtime_id,
332            fence_token,
333            runtime_generation,
334            runtime_epoch_id,
335        }
336    }
337
338    pub fn agent_runtime_id(&self) -> Option<&str> {
339        self.agent_runtime_id.as_deref()
340    }
341
342    pub fn fence_token(&self) -> Option<u64> {
343        self.fence_token
344    }
345
346    pub fn runtime_generation(&self) -> Option<u64> {
347        self.runtime_generation
348    }
349
350    pub fn runtime_epoch_id(&self) -> Option<&str> {
351        self.runtime_epoch_id.as_deref()
352    }
353}
354
355/// Durable read-back shape for machine-owned lifecycle state.
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct MachineLifecycleSnapshot {
358    runtime_state: RuntimeState,
359    binding: MachineLifecycleBindingFacts,
360    supervisor_authority: SupervisorAuthoritySnapshot,
361    unregister_progress: Option<MachineUnregisterProgressSnapshot>,
362}
363
364/// Durable generated unregister-saga progress needed to resume an interrupted
365/// Draining epoch without reconstructing missing producer outcomes in shell
366/// code.
367#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct MachineUnregisterProgressSnapshot {
369    runtime_loop_drain_pending: bool,
370    comms_drain_exit_pending: bool,
371    completion_waiter_drain_pending: bool,
372    runtime_loop_forced_abort: bool,
373    comms_drain_forced_abort: bool,
374}
375
376impl MachineUnregisterProgressSnapshot {
377    pub(crate) fn new(
378        runtime_loop_drain_pending: bool,
379        comms_drain_exit_pending: bool,
380        completion_waiter_drain_pending: bool,
381        runtime_loop_forced_abort: bool,
382        comms_drain_forced_abort: bool,
383    ) -> Self {
384        Self {
385            runtime_loop_drain_pending,
386            comms_drain_exit_pending,
387            completion_waiter_drain_pending,
388            runtime_loop_forced_abort,
389            comms_drain_forced_abort,
390        }
391    }
392
393    pub(crate) fn runtime_loop_drain_pending(&self) -> bool {
394        self.runtime_loop_drain_pending
395    }
396
397    pub(crate) fn comms_drain_exit_pending(&self) -> bool {
398        self.comms_drain_exit_pending
399    }
400
401    pub(crate) fn completion_waiter_drain_pending(&self) -> bool {
402        self.completion_waiter_drain_pending
403    }
404
405    pub(crate) fn runtime_loop_forced_abort(&self) -> bool {
406        self.runtime_loop_forced_abort
407    }
408
409    pub(crate) fn comms_drain_forced_abort(&self) -> bool {
410        self.comms_drain_forced_abort
411    }
412}
413
414impl MachineLifecycleSnapshot {
415    pub(crate) fn new(
416        runtime_state: RuntimeState,
417        binding: MachineLifecycleBindingFacts,
418        supervisor_authority: SupervisorAuthoritySnapshot,
419    ) -> Self {
420        Self::new_with_unregister_progress(runtime_state, binding, supervisor_authority, None)
421    }
422
423    pub(crate) fn new_with_unregister_progress(
424        runtime_state: RuntimeState,
425        binding: MachineLifecycleBindingFacts,
426        supervisor_authority: SupervisorAuthoritySnapshot,
427        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
428    ) -> Self {
429        Self {
430            runtime_state,
431            binding,
432            supervisor_authority,
433            unregister_progress,
434        }
435    }
436
437    /// Runtime state selected by the owning MeerkatMachine transition.
438    pub fn runtime_state(&self) -> RuntimeState {
439        self.runtime_state
440    }
441
442    /// Runtime binding facts selected by the owning MeerkatMachine transition.
443    pub fn binding(&self) -> &MachineLifecycleBindingFacts {
444        &self.binding
445    }
446
447    pub fn supervisor_authority(&self) -> &SupervisorAuthoritySnapshot {
448        &self.supervisor_authority
449    }
450
451    pub fn unregister_progress(&self) -> Option<&MachineUnregisterProgressSnapshot> {
452        self.unregister_progress.as_ref()
453    }
454}
455
456#[allow(
457    clippy::option_option,
458    reason = "serde distinguishes missing from explicit null"
459)]
460fn deserialize_present_nullable<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
461where
462    D: serde::Deserializer<'de>,
463    T: serde::Deserialize<'de>,
464{
465    <Option<T> as serde::Deserialize>::deserialize(deserializer).map(Some)
466}
467
468#[allow(
469    clippy::option_option,
470    reason = "serde distinguishes missing from explicit null"
471)]
472fn require_present_nullable<T>(
473    value: Option<Option<T>>,
474    field: &str,
475) -> Result<Option<T>, RuntimeStoreError> {
476    value.ok_or_else(|| {
477        RuntimeStoreError::ReadFailed(format!(
478            "machine lifecycle field {field} is required (explicit null is allowed)"
479        ))
480    })
481}
482
483#[derive(serde::Serialize, serde::Deserialize)]
484#[serde(deny_unknown_fields)]
485struct MachineLifecycleBindingFactsStoreWire {
486    #[allow(
487        clippy::option_option,
488        reason = "serde distinguishes missing from explicit null"
489    )]
490    #[serde(default, deserialize_with = "deserialize_present_nullable")]
491    agent_runtime_id: Option<Option<String>>,
492    #[allow(
493        clippy::option_option,
494        reason = "serde distinguishes missing from explicit null"
495    )]
496    #[serde(default, deserialize_with = "deserialize_present_nullable")]
497    fence_token: Option<Option<u64>>,
498    #[allow(
499        clippy::option_option,
500        reason = "serde distinguishes missing from explicit null"
501    )]
502    #[serde(default, deserialize_with = "deserialize_present_nullable")]
503    runtime_generation: Option<Option<u64>>,
504    #[allow(
505        clippy::option_option,
506        reason = "serde distinguishes missing from explicit null"
507    )]
508    #[serde(default, deserialize_with = "deserialize_present_nullable")]
509    runtime_epoch_id: Option<Option<String>>,
510}
511
512#[derive(serde::Deserialize)]
513#[serde(deny_unknown_fields)]
514struct MachineLifecycleBindingFactsStoreWireV1 {
515    agent_runtime_id: Option<String>,
516    fence_token: Option<u64>,
517    runtime_generation: Option<u64>,
518    runtime_epoch_id: Option<String>,
519}
520
521impl From<&MachineLifecycleBindingFacts> for MachineLifecycleBindingFactsStoreWire {
522    fn from(binding: &MachineLifecycleBindingFacts) -> Self {
523        Self {
524            agent_runtime_id: Some(binding.agent_runtime_id().map(ToOwned::to_owned)),
525            fence_token: Some(binding.fence_token()),
526            runtime_generation: Some(binding.runtime_generation()),
527            runtime_epoch_id: Some(binding.runtime_epoch_id().map(ToOwned::to_owned)),
528        }
529    }
530}
531
532impl TryFrom<MachineLifecycleBindingFactsStoreWire> for MachineLifecycleBindingFacts {
533    type Error = RuntimeStoreError;
534
535    fn try_from(binding: MachineLifecycleBindingFactsStoreWire) -> Result<Self, Self::Error> {
536        Ok(Self::new(
537            require_present_nullable(binding.agent_runtime_id, "binding.agent_runtime_id")?,
538            require_present_nullable(binding.fence_token, "binding.fence_token")?,
539            require_present_nullable(binding.runtime_generation, "binding.runtime_generation")?,
540            require_present_nullable(binding.runtime_epoch_id, "binding.runtime_epoch_id")?,
541        ))
542    }
543}
544
545impl From<MachineLifecycleBindingFactsStoreWireV1> for MachineLifecycleBindingFacts {
546    fn from(binding: MachineLifecycleBindingFactsStoreWireV1) -> Self {
547        Self::new(
548            binding.agent_runtime_id,
549            binding.fence_token,
550            binding.runtime_generation,
551            binding.runtime_epoch_id,
552        )
553    }
554}
555
556#[derive(serde::Serialize)]
557#[serde(deny_unknown_fields)]
558struct MachineLifecycleSnapshotStoreWire {
559    record_version: u16,
560    runtime_state: RuntimeState,
561    binding: MachineLifecycleBindingFactsStoreWire,
562    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
563    unregister_progress: Option<MachineUnregisterProgressSnapshotStoreWire>,
564}
565
566#[derive(serde::Deserialize)]
567#[serde(deny_unknown_fields)]
568struct MachineLifecycleSnapshotStoreWireV3 {
569    record_version: u16,
570    runtime_state: RuntimeState,
571    binding: MachineLifecycleBindingFactsStoreWire,
572    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
573    #[allow(
574        clippy::option_option,
575        reason = "serde distinguishes a missing v3 field from explicit null progress"
576    )]
577    #[serde(default, deserialize_with = "deserialize_present_nullable")]
578    unregister_progress: Option<Option<MachineUnregisterProgressSnapshotStoreWire>>,
579}
580
581#[derive(serde::Deserialize)]
582#[serde(deny_unknown_fields)]
583struct MachineLifecycleSnapshotStoreWireV2 {
584    record_version: u16,
585    runtime_state: RuntimeState,
586    binding: MachineLifecycleBindingFactsStoreWire,
587    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
588}
589
590#[derive(serde::Serialize, serde::Deserialize)]
591#[serde(deny_unknown_fields)]
592struct MachineUnregisterProgressSnapshotStoreWire {
593    runtime_loop_drain_pending: bool,
594    comms_drain_exit_pending: bool,
595    completion_waiter_drain_pending: bool,
596    runtime_loop_forced_abort: bool,
597    comms_drain_forced_abort: bool,
598}
599
600impl From<&MachineUnregisterProgressSnapshot> for MachineUnregisterProgressSnapshotStoreWire {
601    fn from(snapshot: &MachineUnregisterProgressSnapshot) -> Self {
602        Self {
603            runtime_loop_drain_pending: snapshot.runtime_loop_drain_pending(),
604            comms_drain_exit_pending: snapshot.comms_drain_exit_pending(),
605            completion_waiter_drain_pending: snapshot.completion_waiter_drain_pending(),
606            runtime_loop_forced_abort: snapshot.runtime_loop_forced_abort(),
607            comms_drain_forced_abort: snapshot.comms_drain_forced_abort(),
608        }
609    }
610}
611
612impl From<MachineUnregisterProgressSnapshotStoreWire> for MachineUnregisterProgressSnapshot {
613    fn from(snapshot: MachineUnregisterProgressSnapshotStoreWire) -> Self {
614        Self::new(
615            snapshot.runtime_loop_drain_pending,
616            snapshot.comms_drain_exit_pending,
617            snapshot.completion_waiter_drain_pending,
618            snapshot.runtime_loop_forced_abort,
619            snapshot.comms_drain_forced_abort,
620        )
621    }
622}
623
624/// Exact pre-supervisor-authority lifecycle shape. Version 1 is decoded only
625/// through this migration carrier so a missing authority on a current record
626/// cannot be confused with legacy data.
627#[derive(serde::Deserialize)]
628#[serde(deny_unknown_fields)]
629struct MachineLifecycleSnapshotStoreWireV1 {
630    record_version: u16,
631    runtime_state: RuntimeState,
632    binding: MachineLifecycleBindingFactsStoreWireV1,
633}
634
635#[derive(serde::Deserialize)]
636struct MachineLifecycleSnapshotStoreVersionProbe {
637    record_version: u16,
638}
639
640#[derive(Default, serde::Serialize, serde::Deserialize)]
641#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
642enum SupervisorAuthoritySnapshotStoreWire {
643    #[default]
644    UnboundNoReceipt,
645    Bound {
646        binding: SupervisorBindingReceiptStoreWire,
647    },
648    RevocationPending {
649        pending: SupervisorRevocationPendingReceiptStoreWire,
650    },
651    RotationOperation {
652        rotation: SupervisorRotationReceiptStoreWire,
653    },
654    RevokedReceipt {
655        receipt: RevokedSupervisorReceiptStoreWire,
656    },
657    WithRotationHistory {
658        current: Box<SupervisorAuthoritySnapshotStoreWire>,
659        terminal_receipts: Vec<SupervisorRotationReceiptStoreWire>,
660    },
661}
662
663#[derive(serde::Serialize, serde::Deserialize)]
664#[serde(deny_unknown_fields)]
665struct SupervisorBindingReceiptStoreWire {
666    name: String,
667    peer_id: String,
668    address: String,
669    signing_public_key: String,
670    epoch: u64,
671}
672
673impl From<&SupervisorBindingReceipt> for SupervisorBindingReceiptStoreWire {
674    fn from(receipt: &SupervisorBindingReceipt) -> Self {
675        Self {
676            name: receipt.name().to_owned(),
677            peer_id: receipt.peer_id().to_owned(),
678            address: receipt.address().to_owned(),
679            signing_public_key: receipt.signing_public_key().to_owned(),
680            epoch: receipt.epoch(),
681        }
682    }
683}
684
685impl From<SupervisorBindingReceiptStoreWire> for SupervisorBindingReceipt {
686    fn from(receipt: SupervisorBindingReceiptStoreWire) -> Self {
687        Self::new(
688            receipt.name,
689            receipt.peer_id,
690            receipt.address,
691            receipt.signing_public_key,
692            receipt.epoch,
693        )
694    }
695}
696
697#[derive(serde::Serialize, serde::Deserialize)]
698#[serde(deny_unknown_fields)]
699struct RevokedSupervisorReceiptStoreWire {
700    peer_id: String,
701    signing_public_key: String,
702    epoch: u64,
703}
704
705#[derive(serde::Serialize, serde::Deserialize)]
706#[serde(deny_unknown_fields)]
707struct SupervisorRevocationPendingReceiptStoreWire {
708    name: String,
709    peer_id: String,
710    address: String,
711    signing_public_key: String,
712    epoch: u64,
713}
714
715#[derive(serde::Serialize, serde::Deserialize)]
716#[serde(deny_unknown_fields)]
717struct SupervisorRotationReceiptStoreWire {
718    operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
719    phase: SupervisorRotationPersistencePhase,
720    #[allow(
721        clippy::option_option,
722        reason = "serde distinguishes missing from explicit null"
723    )]
724    #[serde(default, deserialize_with = "deserialize_present_nullable")]
725    rejection: Option<Option<SupervisorRotationRejection>>,
726    previous: SupervisorBindingReceiptStoreWire,
727    next: SupervisorBindingReceiptStoreWire,
728}
729
730impl From<&SupervisorRotationReceipt> for SupervisorRotationReceiptStoreWire {
731    fn from(receipt: &SupervisorRotationReceipt) -> Self {
732        Self {
733            operation_id: receipt.operation_id(),
734            phase: receipt.phase(),
735            rejection: Some(receipt.rejection()),
736            previous: receipt.previous().into(),
737            next: receipt.next().into(),
738        }
739    }
740}
741
742impl TryFrom<SupervisorRotationReceiptStoreWire> for SupervisorRotationReceipt {
743    type Error = RuntimeStoreError;
744
745    fn try_from(receipt: SupervisorRotationReceiptStoreWire) -> Result<Self, Self::Error> {
746        Ok(Self::new(
747            receipt.operation_id,
748            receipt.phase,
749            require_present_nullable(receipt.rejection, "supervisor_authority.rotation.rejection")?,
750            receipt.previous.into(),
751            receipt.next.into(),
752        ))
753    }
754}
755
756impl From<&SupervisorRevocationPendingReceipt> for SupervisorRevocationPendingReceiptStoreWire {
757    fn from(receipt: &SupervisorRevocationPendingReceipt) -> Self {
758        Self {
759            name: receipt.name().to_owned(),
760            peer_id: receipt.peer_id().to_owned(),
761            address: receipt.address().to_owned(),
762            signing_public_key: receipt.signing_public_key().to_owned(),
763            epoch: receipt.epoch(),
764        }
765    }
766}
767
768impl From<SupervisorRevocationPendingReceiptStoreWire> for SupervisorRevocationPendingReceipt {
769    fn from(receipt: SupervisorRevocationPendingReceiptStoreWire) -> Self {
770        Self::new(
771            receipt.name,
772            receipt.peer_id,
773            receipt.address,
774            receipt.signing_public_key,
775            receipt.epoch,
776        )
777    }
778}
779
780impl From<&RevokedSupervisorReceipt> for RevokedSupervisorReceiptStoreWire {
781    fn from(receipt: &RevokedSupervisorReceipt) -> Self {
782        Self {
783            peer_id: receipt.peer_id().to_owned(),
784            signing_public_key: receipt.signing_public_key().to_owned(),
785            epoch: receipt.epoch(),
786        }
787    }
788}
789
790impl From<RevokedSupervisorReceiptStoreWire> for RevokedSupervisorReceipt {
791    fn from(receipt: RevokedSupervisorReceiptStoreWire) -> Self {
792        Self::new(receipt.peer_id, receipt.signing_public_key, receipt.epoch)
793    }
794}
795
796impl From<&SupervisorAuthoritySnapshot> for SupervisorAuthoritySnapshotStoreWire {
797    fn from(snapshot: &SupervisorAuthoritySnapshot) -> Self {
798        match snapshot {
799            SupervisorAuthoritySnapshot::UnboundNoReceipt => Self::UnboundNoReceipt,
800            SupervisorAuthoritySnapshot::Bound(binding) => Self::Bound {
801                binding: binding.into(),
802            },
803            SupervisorAuthoritySnapshot::RevocationPending(pending) => Self::RevocationPending {
804                pending: pending.into(),
805            },
806            SupervisorAuthoritySnapshot::RotationOperation(rotation) => Self::RotationOperation {
807                rotation: rotation.into(),
808            },
809            SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Self::RevokedReceipt {
810                receipt: receipt.into(),
811            },
812            SupervisorAuthoritySnapshot::WithRotationHistory {
813                current,
814                terminal_receipts,
815            } => Self::WithRotationHistory {
816                current: Box::new(current.as_ref().into()),
817                terminal_receipts: terminal_receipts.values().map(Into::into).collect(),
818            },
819        }
820    }
821}
822
823fn supervisor_authority_read_error(
824    context: &str,
825    detail: impl std::fmt::Display,
826) -> RuntimeStoreError {
827    RuntimeStoreError::ReadFailed(format!("{context}: {detail}"))
828}
829
830fn validate_supervisor_descriptor(
831    name: &str,
832    peer_id: &str,
833    address: &str,
834    signing_public_key: &str,
835    context: &str,
836) -> Result<(), RuntimeStoreError> {
837    let pubkey = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
838        .map_err(|error| supervisor_authority_read_error(context, error))?;
839    let spec = meerkat_contracts::wire::supervisor_bridge::BridgePeerSpec {
840        name: name.to_owned(),
841        peer_id: peer_id.to_owned(),
842        address: address.to_owned(),
843        pubkey,
844    };
845    meerkat_core::comms::TrustedPeerDescriptor::try_from(&spec)
846        .map(|_| ())
847        .map_err(|error| supervisor_authority_read_error(context, error))
848}
849
850fn validate_supervisor_binding_receipt(
851    receipt: &SupervisorBindingReceipt,
852    context: &str,
853) -> Result<(), RuntimeStoreError> {
854    validate_supervisor_descriptor(
855        receipt.name(),
856        receipt.peer_id(),
857        receipt.address(),
858        receipt.signing_public_key(),
859        context,
860    )
861}
862
863fn validate_revoked_supervisor_receipt(
864    receipt: &RevokedSupervisorReceipt,
865    context: &str,
866) -> Result<(), RuntimeStoreError> {
867    let pubkey =
868        crate::comms_drain::decode_supervisor_signing_public_key(receipt.signing_public_key())
869            .map_err(|error| supervisor_authority_read_error(context, error))?;
870    if pubkey.iter().all(|byte| *byte == 0) {
871        return Err(supervisor_authority_read_error(
872            context,
873            "supervisor signing public key must be non-zero",
874        ));
875    }
876    let peer_id = meerkat_core::comms::PeerId::parse(receipt.peer_id())
877        .map_err(|error| supervisor_authority_read_error(context, error))?;
878    let derived = meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey);
879    if peer_id != derived {
880        return Err(supervisor_authority_read_error(
881            context,
882            format!("peer id {peer_id} does not match signing-key-derived id {derived}"),
883        ));
884    }
885    Ok(())
886}
887
888fn validate_supervisor_rotation_receipt(
889    receipt: &SupervisorRotationReceipt,
890    terminal_history: bool,
891) -> Result<(), RuntimeStoreError> {
892    let operation_id = receipt.operation_id();
893    if operation_id.as_uuid().is_nil() {
894        return Err(supervisor_authority_read_error(
895            "supervisor rotation operation",
896            "operation id must not be the nil UUID",
897        ));
898    }
899    validate_supervisor_binding_receipt(
900        receipt.previous(),
901        &format!("supervisor rotation {operation_id} previous authority is invalid"),
902    )?;
903
904    let rejection_matches = matches!(
905        (receipt.phase(), receipt.rejection()),
906        (
907            SupervisorRotationPersistencePhase::PreviousRevokePending
908                | SupervisorRotationPersistencePhase::NextPublishPending
909                | SupervisorRotationPersistencePhase::Completed,
910            None
911        ) | (SupervisorRotationPersistencePhase::Rejected, Some(_))
912    );
913    if !rejection_matches {
914        return Err(supervisor_authority_read_error(
915            "supervisor rotation operation",
916            format!("{operation_id} has inconsistent rejection state"),
917        ));
918    }
919    if terminal_history
920        && !matches!(
921            receipt.phase(),
922            SupervisorRotationPersistencePhase::Completed
923                | SupervisorRotationPersistencePhase::Rejected
924        )
925    {
926        return Err(supervisor_authority_read_error(
927            "supervisor rotation history",
928            format!("{operation_id} is not terminal"),
929        ));
930    }
931
932    match receipt.phase() {
933        SupervisorRotationPersistencePhase::PreviousRevokePending
934        | SupervisorRotationPersistencePhase::NextPublishPending => {
935            validate_supervisor_binding_receipt(
936                receipt.next(),
937                &format!("supervisor rotation {operation_id} target is invalid"),
938            )?;
939            if receipt.next().epoch() <= receipt.previous().epoch() {
940                return Err(supervisor_authority_read_error(
941                    "supervisor rotation operation",
942                    format!(
943                        "{operation_id} target epoch {} does not advance previous epoch {}",
944                        receipt.next().epoch(),
945                        receipt.previous().epoch()
946                    ),
947                ));
948            }
949        }
950        SupervisorRotationPersistencePhase::Completed => {
951            validate_supervisor_binding_receipt(
952                receipt.next(),
953                &format!("supervisor rotation {operation_id} target is invalid"),
954            )?;
955            // A legacy member may already have the exact target installed
956            // before the operation protocol assigns an id. Its adoption
957            // receipt is Completed with an exact previous == next witness.
958            let exact_current_adoption = receipt.previous() == receipt.next();
959            if !exact_current_adoption && receipt.next().epoch() <= receipt.previous().epoch() {
960                return Err(supervisor_authority_read_error(
961                    "supervisor rotation operation",
962                    format!(
963                        "{operation_id} completed target epoch {} does not advance previous epoch {}",
964                        receipt.next().epoch(),
965                        receipt.previous().epoch()
966                    ),
967                ));
968            }
969        }
970        SupervisorRotationPersistencePhase::Rejected => {
971            let Some(rejection) = receipt.rejection() else {
972                return Err(supervisor_authority_read_error(
973                    "supervisor rotation operation",
974                    format!("{operation_id} rejected without a rejection class"),
975                ));
976            };
977            match rejection {
978                SupervisorRotationRejection::InvalidTarget
979                | SupervisorRotationRejection::UnsupportedProtocolVersion => {
980                    // These two rejection classes retain the undecodable target
981                    // fields as raw evidence. They are deliberately exempt from
982                    // target descriptor validation and epoch comparison.
983                }
984                SupervisorRotationRejection::TargetEpochNotAdvanced => {
985                    validate_supervisor_binding_receipt(
986                        receipt.next(),
987                        &format!("supervisor rotation {operation_id} rejected target is invalid"),
988                    )?;
989                    if receipt.next().epoch() > receipt.previous().epoch() {
990                        return Err(supervisor_authority_read_error(
991                            "supervisor rotation operation",
992                            format!(
993                                "{operation_id} rejected as non-advancing but target epoch {} advances previous epoch {}",
994                                receipt.next().epoch(),
995                                receipt.previous().epoch()
996                            ),
997                        ));
998                    }
999                }
1000                SupervisorRotationRejection::OperationConflict
1001                | SupervisorRotationRejection::NotBound
1002                | SupervisorRotationRejection::SenderMismatch => {
1003                    return Err(supervisor_authority_read_error(
1004                        "supervisor rotation operation",
1005                        format!(
1006                            "{operation_id} transient rejection {rejection:?} must not be persisted as a durable receipt"
1007                        ),
1008                    ));
1009                }
1010            }
1011        }
1012    }
1013    Ok(())
1014}
1015
1016type SupervisorEpochKeyIndex = std::collections::BTreeMap<u64, [u8; 32]>;
1017
1018fn record_supervisor_epoch_key(
1019    epochs: &mut SupervisorEpochKeyIndex,
1020    epoch: u64,
1021    signing_public_key: &str,
1022    context: &str,
1023) -> Result<(), RuntimeStoreError> {
1024    let key = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
1025        .map_err(|error| supervisor_authority_read_error(context, error))?;
1026    if let Some(existing) = epochs.get(&epoch) {
1027        if existing != &key {
1028            return Err(supervisor_authority_read_error(
1029                context,
1030                format!("epoch {epoch} is bound to conflicting supervisor signing keys"),
1031            ));
1032        }
1033    } else {
1034        epochs.insert(epoch, key);
1035    }
1036    Ok(())
1037}
1038
1039fn record_supervisor_binding_epoch(
1040    epochs: &mut SupervisorEpochKeyIndex,
1041    receipt: &SupervisorBindingReceipt,
1042    context: &str,
1043) -> Result<(), RuntimeStoreError> {
1044    record_supervisor_epoch_key(
1045        epochs,
1046        receipt.epoch(),
1047        receipt.signing_public_key(),
1048        context,
1049    )
1050}
1051
1052fn record_rotation_authoritative_epochs(
1053    epochs: &mut SupervisorEpochKeyIndex,
1054    receipt: &SupervisorRotationReceipt,
1055    context: &str,
1056) -> Result<(), RuntimeStoreError> {
1057    record_supervisor_binding_epoch(epochs, receipt.previous(), context)?;
1058    if matches!(
1059        receipt.phase(),
1060        SupervisorRotationPersistencePhase::PreviousRevokePending
1061            | SupervisorRotationPersistencePhase::NextPublishPending
1062            | SupervisorRotationPersistencePhase::Completed
1063    ) {
1064        record_supervisor_binding_epoch(epochs, receipt.next(), context)?;
1065    }
1066    Ok(())
1067}
1068
1069fn record_current_authoritative_epochs(
1070    epochs: &mut SupervisorEpochKeyIndex,
1071    current: &SupervisorAuthoritySnapshot,
1072) -> Result<(), RuntimeStoreError> {
1073    match current {
1074        SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1075        SupervisorAuthoritySnapshot::Bound(binding) => {
1076            record_supervisor_binding_epoch(epochs, binding, "current supervisor authority")
1077        }
1078        SupervisorAuthoritySnapshot::RevocationPending(pending) => record_supervisor_epoch_key(
1079            epochs,
1080            pending.epoch(),
1081            pending.signing_public_key(),
1082            "current pending supervisor revocation authority",
1083        ),
1084        SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1085            record_rotation_authoritative_epochs(
1086                epochs,
1087                rotation,
1088                "current supervisor rotation authority",
1089            )
1090        }
1091        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => record_supervisor_epoch_key(
1092            epochs,
1093            receipt.epoch(),
1094            receipt.signing_public_key(),
1095            "current revoked supervisor authority",
1096        ),
1097        SupervisorAuthoritySnapshot::WithRotationHistory { .. } => {
1098            Err(RuntimeStoreError::ReadFailed(
1099                "nested supervisor rotation history is not canonical".to_string(),
1100            ))
1101        }
1102    }
1103}
1104
1105fn current_supervisor_epoch(current: &SupervisorAuthoritySnapshot) -> Option<u64> {
1106    match current {
1107        SupervisorAuthoritySnapshot::UnboundNoReceipt => None,
1108        SupervisorAuthoritySnapshot::Bound(binding) => Some(binding.epoch()),
1109        SupervisorAuthoritySnapshot::RevocationPending(pending) => Some(pending.epoch()),
1110        SupervisorAuthoritySnapshot::RotationOperation(rotation) => Some(match rotation.phase() {
1111            SupervisorRotationPersistencePhase::PreviousRevokePending
1112            | SupervisorRotationPersistencePhase::Rejected => rotation.previous().epoch(),
1113            SupervisorRotationPersistencePhase::NextPublishPending
1114            | SupervisorRotationPersistencePhase::Completed => rotation.next().epoch(),
1115        }),
1116        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Some(receipt.epoch()),
1117        SupervisorAuthoritySnapshot::WithRotationHistory { .. } => None,
1118    }
1119}
1120
1121fn terminal_rotation_authority_epoch(receipt: &SupervisorRotationReceipt) -> u64 {
1122    match receipt.phase() {
1123        SupervisorRotationPersistencePhase::Completed => receipt.next().epoch(),
1124        SupervisorRotationPersistencePhase::Rejected => receipt.previous().epoch(),
1125        SupervisorRotationPersistencePhase::PreviousRevokePending
1126        | SupervisorRotationPersistencePhase::NextPublishPending => receipt.previous().epoch(),
1127    }
1128}
1129
1130fn validate_supervisor_rotation_history_coherence(
1131    current: &SupervisorAuthoritySnapshot,
1132    terminal_receipts: &std::collections::BTreeMap<
1133        meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1134        SupervisorRotationReceipt,
1135    >,
1136) -> Result<(), RuntimeStoreError> {
1137    let Some(current_epoch) = current_supervisor_epoch(current) else {
1138        return Err(RuntimeStoreError::ReadFailed(
1139            "supervisor rotation history requires a current authority epoch".to_string(),
1140        ));
1141    };
1142
1143    let mut epochs = SupervisorEpochKeyIndex::new();
1144    record_current_authoritative_epochs(&mut epochs, current)?;
1145    let mut history_high_water = 0;
1146    for receipt in terminal_receipts.values() {
1147        record_rotation_authoritative_epochs(
1148            &mut epochs,
1149            receipt,
1150            "supervisor rotation history authority",
1151        )?;
1152        history_high_water = history_high_water.max(terminal_rotation_authority_epoch(receipt));
1153    }
1154    if current_epoch < history_high_water {
1155        return Err(RuntimeStoreError::ReadFailed(format!(
1156            "current supervisor epoch {current_epoch} is below terminal rotation history high-water {history_high_water}"
1157        )));
1158    }
1159    Ok(())
1160}
1161
1162fn validate_supervisor_authority_snapshot(
1163    snapshot: &SupervisorAuthoritySnapshot,
1164) -> Result<(), RuntimeStoreError> {
1165    match snapshot {
1166        SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1167        SupervisorAuthoritySnapshot::Bound(binding) => {
1168            validate_supervisor_binding_receipt(binding, "bound supervisor is invalid")
1169        }
1170        SupervisorAuthoritySnapshot::RevocationPending(pending) => validate_supervisor_descriptor(
1171            pending.name(),
1172            pending.peer_id(),
1173            pending.address(),
1174            pending.signing_public_key(),
1175            "pending supervisor revocation authority is invalid",
1176        ),
1177        SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1178            validate_supervisor_rotation_receipt(rotation, false)
1179        }
1180        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => {
1181            validate_revoked_supervisor_receipt(receipt, "revoked supervisor receipt is invalid")
1182        }
1183        SupervisorAuthoritySnapshot::WithRotationHistory {
1184            current,
1185            terminal_receipts,
1186        } => {
1187            if matches!(
1188                current.as_ref(),
1189                SupervisorAuthoritySnapshot::WithRotationHistory { .. }
1190            ) {
1191                return Err(RuntimeStoreError::ReadFailed(
1192                    "nested supervisor rotation history is not canonical".to_string(),
1193                ));
1194            }
1195            if terminal_receipts.is_empty() {
1196                return Err(RuntimeStoreError::ReadFailed(
1197                    "empty supervisor rotation history wrapper is not canonical".to_string(),
1198                ));
1199            }
1200            validate_supervisor_authority_snapshot(current)?;
1201            for (operation_id, receipt) in terminal_receipts {
1202                if operation_id != &receipt.operation_id() {
1203                    return Err(RuntimeStoreError::ReadFailed(format!(
1204                        "supervisor rotation history key {operation_id} does not match receipt id {}",
1205                        receipt.operation_id()
1206                    )));
1207                }
1208                validate_supervisor_rotation_receipt(receipt, true)?;
1209            }
1210            if let SupervisorAuthoritySnapshot::RotationOperation(active) = current.as_ref()
1211                && terminal_receipts.contains_key(&active.operation_id())
1212            {
1213                return Err(RuntimeStoreError::ReadFailed(
1214                    "active supervisor rotation is duplicated in terminal history".to_string(),
1215                ));
1216            }
1217            validate_supervisor_rotation_history_coherence(current, terminal_receipts)
1218        }
1219    }
1220}
1221
1222impl TryFrom<SupervisorAuthoritySnapshotStoreWire> for SupervisorAuthoritySnapshot {
1223    type Error = RuntimeStoreError;
1224
1225    fn try_from(snapshot: SupervisorAuthoritySnapshotStoreWire) -> Result<Self, Self::Error> {
1226        match snapshot {
1227            SupervisorAuthoritySnapshotStoreWire::UnboundNoReceipt => Ok(Self::UnboundNoReceipt),
1228            SupervisorAuthoritySnapshotStoreWire::Bound { binding } => {
1229                let binding = binding.into();
1230                validate_supervisor_binding_receipt(&binding, "bound supervisor is invalid")?;
1231                Ok(Self::Bound(binding))
1232            }
1233            SupervisorAuthoritySnapshotStoreWire::RevocationPending { pending } => {
1234                let pending: SupervisorRevocationPendingReceipt = pending.into();
1235                validate_supervisor_descriptor(
1236                    pending.name(),
1237                    pending.peer_id(),
1238                    pending.address(),
1239                    pending.signing_public_key(),
1240                    "pending supervisor revocation authority is invalid",
1241                )?;
1242                Ok(Self::RevocationPending(pending))
1243            }
1244            SupervisorAuthoritySnapshotStoreWire::RotationOperation { rotation } => {
1245                let receipt: SupervisorRotationReceipt = rotation.try_into()?;
1246                validate_supervisor_rotation_receipt(&receipt, false)?;
1247                Ok(Self::RotationOperation(receipt))
1248            }
1249            SupervisorAuthoritySnapshotStoreWire::RevokedReceipt { receipt } => {
1250                let receipt = receipt.into();
1251                validate_revoked_supervisor_receipt(
1252                    &receipt,
1253                    "revoked supervisor receipt is invalid",
1254                )?;
1255                Ok(Self::RevokedReceipt(receipt))
1256            }
1257            SupervisorAuthoritySnapshotStoreWire::WithRotationHistory {
1258                current,
1259                terminal_receipts,
1260            } => {
1261                if terminal_receipts.is_empty() {
1262                    return Err(RuntimeStoreError::ReadFailed(
1263                        "empty supervisor rotation history wrapper is not canonical".to_string(),
1264                    ));
1265                }
1266                let current = Self::try_from(*current)?;
1267                if matches!(current, Self::WithRotationHistory { .. }) {
1268                    return Err(RuntimeStoreError::ReadFailed(
1269                        "nested supervisor rotation history is not canonical".to_string(),
1270                    ));
1271                }
1272                let mut receipts = std::collections::BTreeMap::new();
1273                for wire in terminal_receipts {
1274                    let receipt: SupervisorRotationReceipt = wire.try_into()?;
1275                    validate_supervisor_rotation_receipt(&receipt, true)?;
1276                    if receipts.insert(receipt.operation_id(), receipt).is_some() {
1277                        return Err(RuntimeStoreError::ReadFailed(
1278                            "supervisor rotation history contains a duplicate operation id"
1279                                .to_string(),
1280                        ));
1281                    }
1282                }
1283                if let Self::RotationOperation(active) = &current
1284                    && receipts.contains_key(&active.operation_id())
1285                {
1286                    return Err(RuntimeStoreError::ReadFailed(
1287                        "active supervisor rotation is duplicated in terminal history".to_string(),
1288                    ));
1289                }
1290                let snapshot = Self::WithRotationHistory {
1291                    current: Box::new(current),
1292                    terminal_receipts: receipts,
1293                };
1294                validate_supervisor_authority_snapshot(&snapshot)?;
1295                Ok(snapshot)
1296            }
1297        }
1298    }
1299}
1300
1301impl From<&MachineLifecycleSnapshot> for MachineLifecycleSnapshotStoreWire {
1302    fn from(snapshot: &MachineLifecycleSnapshot) -> Self {
1303        Self {
1304            record_version: MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
1305            runtime_state: snapshot.runtime_state(),
1306            binding: snapshot.binding().into(),
1307            supervisor_authority: snapshot.supervisor_authority().into(),
1308            unregister_progress: snapshot.unregister_progress().map(Into::into),
1309        }
1310    }
1311}
1312
1313fn validate_unregister_progress_snapshot(
1314    progress: Option<&MachineUnregisterProgressSnapshot>,
1315) -> Result<(), RuntimeStoreError> {
1316    if let Some(progress) = progress {
1317        if progress.runtime_loop_drain_pending() && progress.runtime_loop_forced_abort() {
1318            return Err(RuntimeStoreError::ReadFailed(
1319                "unregister runtime-loop forced disposition cannot precede obligation closure"
1320                    .into(),
1321            ));
1322        }
1323        if progress.comms_drain_exit_pending() && progress.comms_drain_forced_abort() {
1324            return Err(RuntimeStoreError::ReadFailed(
1325                "unregister comms-drain forced disposition cannot precede obligation closure"
1326                    .into(),
1327            ));
1328        }
1329    }
1330    Ok(())
1331}
1332
1333impl TryFrom<MachineLifecycleSnapshotStoreWireV3> for MachineLifecycleSnapshot {
1334    type Error = RuntimeStoreError;
1335
1336    fn try_from(record: MachineLifecycleSnapshotStoreWireV3) -> Result<Self, Self::Error> {
1337        if record.record_version != MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1338            return Err(RuntimeStoreError::ReadFailed(format!(
1339                "unsupported machine lifecycle store record version {}",
1340                record.record_version
1341            )));
1342        }
1343        let unregister_progress =
1344            require_present_nullable(record.unregister_progress, "unregister_progress")?
1345                .map(Into::into);
1346        validate_unregister_progress_snapshot(unregister_progress.as_ref())?;
1347        Ok(Self::new_with_unregister_progress(
1348            record.runtime_state,
1349            record.binding.try_into()?,
1350            record.supervisor_authority.try_into()?,
1351            unregister_progress,
1352        ))
1353    }
1354}
1355
1356fn decode_machine_lifecycle_store_record(
1357    bytes: &[u8],
1358) -> Result<MachineLifecycleSnapshot, RuntimeStoreError> {
1359    let version = serde_json::from_slice::<MachineLifecycleSnapshotStoreVersionProbe>(bytes)
1360        .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1361    match version.record_version {
1362        LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1363            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV1>(bytes)
1364                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1365            if record.record_version != LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1366                return Err(RuntimeStoreError::ReadFailed(format!(
1367                    "unsupported machine lifecycle store record version {}",
1368                    record.record_version
1369                )));
1370            }
1371            Ok(MachineLifecycleSnapshot::new(
1372                record.runtime_state,
1373                record.binding.into(),
1374                SupervisorAuthoritySnapshot::UnboundNoReceipt,
1375            ))
1376        }
1377        SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1378            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV2>(bytes)
1379                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1380            if record.record_version != SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1381                return Err(RuntimeStoreError::ReadFailed(format!(
1382                    "unsupported machine lifecycle store record version {}",
1383                    record.record_version
1384                )));
1385            }
1386            Ok(MachineLifecycleSnapshot::new(
1387                record.runtime_state,
1388                record.binding.try_into()?,
1389                record.supervisor_authority.try_into()?,
1390            ))
1391        }
1392        MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1393            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV3>(bytes)
1394                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1395            MachineLifecycleSnapshot::try_from(record)
1396        }
1397        unsupported => Err(RuntimeStoreError::ReadFailed(format!(
1398            "unsupported machine lifecycle store record version {unsupported}"
1399        ))),
1400    }
1401}
1402
1403/// Load the last persisted runtime-state projection from a generated lifecycle
1404/// record.
1405///
1406/// This is a projection of [`MachineLifecycleCommit`] authority. Store
1407/// implementations provide only opaque record bytes; the runtime crate owns the
1408/// decoding and rejects compatibility rows that are not machine lifecycle
1409/// records.
1410pub async fn load_runtime_state(
1411    store: &dyn RuntimeStore,
1412    runtime_id: &LogicalRuntimeId,
1413) -> Result<Option<RuntimeState>, RuntimeStoreError> {
1414    Ok(load_machine_lifecycle(store, runtime_id)
1415        .await?
1416        .map(|snapshot| snapshot.runtime_state()))
1417}
1418
1419pub(crate) async fn load_machine_lifecycle(
1420    store: &dyn RuntimeStore,
1421    runtime_id: &LogicalRuntimeId,
1422) -> Result<Option<MachineLifecycleSnapshot>, RuntimeStoreError> {
1423    store
1424        .load_machine_lifecycle_record(runtime_id)
1425        .await?
1426        .map(|bytes| decode_machine_lifecycle_store_record(&bytes))
1427        .transpose()
1428}
1429
1430/// Declared durable store record for generated machine lifecycle truth.
1431///
1432/// Stores receive this record from [`MachineLifecycleCommit`] and may persist
1433/// its encoded form. Loading must decode this exact record shape; compatibility
1434/// runtime-state projections are not lifecycle authority.
1435#[derive(Debug, Clone, PartialEq, Eq)]
1436pub struct MachineLifecycleStoreRecord {
1437    snapshot: MachineLifecycleSnapshot,
1438}
1439
1440impl MachineLifecycleStoreRecord {
1441    pub(crate) fn from_snapshot(snapshot: &MachineLifecycleSnapshot) -> Self {
1442        Self {
1443            snapshot: snapshot.clone(),
1444        }
1445    }
1446
1447    pub fn encode(&self) -> Result<Vec<u8>, RuntimeStoreError> {
1448        validate_supervisor_authority_snapshot(self.snapshot.supervisor_authority())
1449            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
1450        validate_unregister_progress_snapshot(self.snapshot.unregister_progress())
1451            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
1452        let wire = MachineLifecycleSnapshotStoreWire::from(&self.snapshot);
1453        serde_json::to_vec(&wire).map_err(|err| RuntimeStoreError::WriteFailed(err.to_string()))
1454    }
1455}
1456
1457/// Machine-owned lifecycle commit token.
1458///
1459/// This token has no public constructor. RuntimeStore implementors can persist
1460/// the selected state and binding facts, but callers outside the machine/driver
1461/// commit path cannot select arbitrary lifecycle truth.
1462#[derive(Debug, Clone, PartialEq, Eq)]
1463pub struct MachineLifecycleCommit {
1464    snapshot: MachineLifecycleSnapshot,
1465    /// Exact ops epoch that the final unregister transaction must tombstone.
1466    /// This is transaction metadata, not part of the lifecycle store record.
1467    retired_ops_epoch: Option<meerkat_core::RuntimeEpochId>,
1468}
1469
1470impl MachineLifecycleCommit {
1471    #[cfg(test)]
1472    pub(crate) fn new_with_binding(
1473        runtime_state: RuntimeState,
1474        binding: MachineLifecycleBindingFacts,
1475        supervisor_authority: SupervisorAuthoritySnapshot,
1476    ) -> Self {
1477        Self::new_with_binding_and_unregister_progress(
1478            runtime_state,
1479            binding,
1480            supervisor_authority,
1481            None,
1482        )
1483    }
1484
1485    pub(crate) fn new_with_binding_and_unregister_progress(
1486        runtime_state: RuntimeState,
1487        binding: MachineLifecycleBindingFacts,
1488        supervisor_authority: SupervisorAuthoritySnapshot,
1489        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
1490    ) -> Self {
1491        Self {
1492            snapshot: MachineLifecycleSnapshot::new_with_unregister_progress(
1493                runtime_state,
1494                binding,
1495                supervisor_authority,
1496                unregister_progress,
1497            ),
1498            retired_ops_epoch: None,
1499        }
1500    }
1501
1502    pub(crate) fn for_unregister_finalization(
1503        mut self,
1504        retired_ops_epoch: meerkat_core::RuntimeEpochId,
1505    ) -> Self {
1506        self.retired_ops_epoch = Some(retired_ops_epoch);
1507        self
1508    }
1509
1510    pub(crate) fn retired_ops_epoch(&self) -> Option<&meerkat_core::RuntimeEpochId> {
1511        self.retired_ops_epoch.as_ref()
1512    }
1513
1514    /// Runtime state selected by the owning MeerkatMachine transition.
1515    pub fn runtime_state(&self) -> RuntimeState {
1516        self.snapshot.runtime_state()
1517    }
1518
1519    /// Durable lifecycle snapshot selected by the owning MeerkatMachine transition.
1520    pub fn snapshot(&self) -> &MachineLifecycleSnapshot {
1521        &self.snapshot
1522    }
1523
1524    /// Durable record selected by the owning MeerkatMachine transition.
1525    pub fn store_record(&self) -> MachineLifecycleStoreRecord {
1526        MachineLifecycleStoreRecord::from_snapshot(&self.snapshot)
1527    }
1528
1529    pub(crate) fn into_snapshot(self) -> MachineLifecycleSnapshot {
1530        self.snapshot
1531    }
1532}
1533
1534/// Atomic persistence interface for runtime state.
1535///
1536/// Implementations:
1537/// - `InMemoryRuntimeStore` — in-memory, no durability (ephemeral/testing)
1538/// - `SqliteRuntimeStore` — SQLite-backed durable runtime state
1539#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1540#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1541pub trait RuntimeStore: Send + Sync {
1542    /// Whether [`RuntimeStore::atomic_apply`] durably records typed compaction
1543    /// projection intents in the same boundary as the session rewrite.
1544    /// Unknown/custom stores fail closed by default.
1545    fn supports_compaction_projection_outbox(&self) -> bool {
1546        false
1547    }
1548
1549    /// Stable key for process-local auth/OAuth authority reuse across reopened
1550    /// handles for the same durable store.
1551    fn auth_authority_key(&self) -> Option<String> {
1552        None
1553    }
1554
1555    /// Persist the runtime-owned OAuth login-flow payload snapshot.
1556    ///
1557    /// The AuthMachine owns admission/consume semantics; this payload snapshot
1558    /// carries the PKCE verifier and device-code correlation data needed to
1559    /// rehydrate active flows after a persistent runtime process restart.
1560    fn persist_auth_oauth_flow_snapshot(
1561        &self,
1562        snapshot_json: &[u8],
1563    ) -> Result<(), RuntimeStoreError> {
1564        let _ = snapshot_json;
1565        Err(RuntimeStoreError::Unsupported(
1566            "persist_auth_oauth_flow_snapshot".into(),
1567        ))
1568    }
1569
1570    /// Load the runtime-owned OAuth login-flow payload snapshot, if present.
1571    fn load_auth_oauth_flow_snapshot(&self) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
1572        Err(RuntimeStoreError::Unsupported(
1573            "load_auth_oauth_flow_snapshot".into(),
1574        ))
1575    }
1576
1577    /// Atomically update the runtime-owned OAuth login-flow payload snapshot.
1578    ///
1579    /// Stores that support OAuth snapshots must override this with a lock,
1580    /// transaction, or compare-and-swap boundary. A load/compute/persist
1581    /// fallback is not safe for admission, capacity, or consume claims.
1582    fn update_auth_oauth_flow_snapshot(
1583        &self,
1584        _update: &mut AuthOAuthFlowSnapshotUpdate<'_>,
1585    ) -> Result<(), RuntimeStoreError> {
1586        Err(RuntimeStoreError::Unsupported(
1587            "update_auth_oauth_flow_snapshot".into(),
1588        ))
1589    }
1590
1591    /// Atomically persist a session snapshot that is not a run boundary.
1592    ///
1593    /// Session-control snapshots update durable session authority without
1594    /// producing a [`RunBoundaryReceipt`].
1595    async fn commit_session_snapshot(
1596        &self,
1597        runtime_id: &LogicalRuntimeId,
1598        session_delta: SessionDelta,
1599    ) -> Result<(), RuntimeStoreError>;
1600
1601    /// Atomically persist a same-session transcript rewrite snapshot.
1602    ///
1603    /// Store implementations that support transcript edits must compare the
1604    /// currently persisted session transcript revision with `commit.parent_revision`
1605    /// inside the same lock or transaction that writes `session_delta`.
1606    async fn commit_session_transcript_rewrite_snapshot(
1607        &self,
1608        runtime_id: &LogicalRuntimeId,
1609        session_delta: SessionDelta,
1610        commit: &meerkat_core::TranscriptRewriteCommit,
1611    ) -> Result<(), RuntimeStoreError> {
1612        let _ = (runtime_id, session_delta, commit);
1613        Err(RuntimeStoreError::Unsupported(
1614            "commit_session_transcript_rewrite_snapshot".into(),
1615        ))
1616    }
1617
1618    /// Atomically persist session delta + receipt + input state updates.
1619    ///
1620    /// All three writes MUST commit in a single atomic operation.
1621    /// If any write fails, none should be visible.
1622    /// Atomically persist session delta + receipt + input state updates.
1623    ///
1624    /// All writes MUST commit in a single atomic operation.
1625    /// If `session_store_key` is `Some`, validates that the snapshot belongs
1626    /// to that session and, for stores that physically share a `SessionStore`
1627    /// table, writes that table in the same transaction. Runtime snapshot
1628    /// authority remains keyed only by `runtime_id`; `session_store_key` must
1629    /// not create a raw session UUID runtime alias.
1630    /// Compaction intents must be inserted as pending outbox rows in this same
1631    /// boundary. An intent whose exact outbox identity is already finalized is
1632    /// a stale snapshot replay and must be rejected without mutating any part
1633    /// of the boundary.
1634    async fn atomic_apply(
1635        &self,
1636        runtime_id: &LogicalRuntimeId,
1637        session_delta: Option<SessionDelta>,
1638        receipt: RunBoundaryReceipt,
1639        input_updates: Vec<InputStatePersistenceRecord>,
1640        session_store_key: Option<meerkat_core::types::SessionId>,
1641    ) -> Result<(), RuntimeStoreError>;
1642
1643    /// Load exact compaction projection intents committed by atomic_apply but
1644    /// not yet acknowledged as finalized by the memory store.
1645    async fn load_pending_compaction_projections(
1646        &self,
1647        runtime_id: &LogicalRuntimeId,
1648    ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
1649        let _ = runtime_id;
1650        Err(RuntimeStoreError::Unsupported(
1651            "load_pending_compaction_projections".to_string(),
1652        ))
1653    }
1654
1655    /// Idempotently acknowledge post-commit memory finalization.
1656    ///
1657    /// The acknowledgement and removal of this exact intent from the
1658    /// authoritative persisted session snapshot MUST occur in one atomic
1659    /// boundary. The finalized outbox row remains as a tombstone so later
1660    /// snapshot writes can reject stale metadata replay.
1661    async fn mark_compaction_projection_finalized(
1662        &self,
1663        runtime_id: &LogicalRuntimeId,
1664        projection: &meerkat_core::CompactionProjectionId,
1665    ) -> Result<(), RuntimeStoreError> {
1666        let _ = (runtime_id, projection);
1667        Err(RuntimeStoreError::Unsupported(
1668            "mark_compaction_projection_finalized".to_string(),
1669        ))
1670    }
1671
1672    /// Load all input states for a runtime.
1673    async fn load_input_states(
1674        &self,
1675        runtime_id: &LogicalRuntimeId,
1676    ) -> Result<Vec<StoredInputState>, RuntimeStoreError>;
1677
1678    /// Load a specific boundary receipt.
1679    async fn load_boundary_receipt(
1680        &self,
1681        runtime_id: &LogicalRuntimeId,
1682        run_id: &RunId,
1683        sequence: u64,
1684    ) -> Result<Option<RunBoundaryReceipt>, RuntimeStoreError>;
1685
1686    /// Load the latest committed session snapshot for a runtime, if any.
1687    async fn load_session_snapshot(
1688        &self,
1689        runtime_id: &LogicalRuntimeId,
1690    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
1691
1692    /// Remove the latest committed session snapshot for a runtime.
1693    ///
1694    /// This is used only as a fail-closed quarantine path after a compatibility
1695    /// projection write rejects a runtime snapshot that was already staged as
1696    /// runtime authority and the service cannot restore the previous snapshot.
1697    async fn clear_session_snapshot(
1698        &self,
1699        runtime_id: &LogicalRuntimeId,
1700    ) -> Result<(), RuntimeStoreError>;
1701
1702    /// Replace the latest committed session snapshot only if it still matches
1703    /// `expected_current`.
1704    ///
1705    /// Used by fail-closed recovery after a compatibility projection rejected
1706    /// a runtime snapshot. Implementations must compare and write atomically so
1707    /// recovery cannot overwrite a newer runtime-authoritative snapshot.
1708    async fn replace_session_snapshot_if_current(
1709        &self,
1710        runtime_id: &LogicalRuntimeId,
1711        expected_current: &[u8],
1712        replacement: Vec<u8>,
1713    ) -> Result<bool, RuntimeStoreError>;
1714
1715    /// Remove the latest committed session snapshot only if it still matches
1716    /// `expected_current`.
1717    ///
1718    /// This is the conditional variant of the fail-closed quarantine path.
1719    async fn clear_session_snapshot_if_current(
1720        &self,
1721        runtime_id: &LogicalRuntimeId,
1722        expected_current: &[u8],
1723    ) -> Result<bool, RuntimeStoreError>;
1724
1725    /// Report whether the runtime-projection fallback for `runtime_id` is
1726    /// quarantined.
1727    ///
1728    /// This is a durable single-owner fact: when
1729    /// [`clear_session_snapshot_if_current`](Self::clear_session_snapshot_if_current)
1730    /// matches and DELETEs a rejected runtime snapshot, the same atomic boundary
1731    /// records the quarantine marker. A subsequent live snapshot write clears it.
1732    /// Recovery reads this to decide whether a store-only projection may stand in
1733    /// for an absent runtime snapshot. The default is fail-safe (`false`): stores
1734    /// that cannot record the marker durably never claim a snapshot is
1735    /// quarantined.
1736    async fn is_runtime_projection_quarantined(
1737        &self,
1738        runtime_id: &LogicalRuntimeId,
1739    ) -> Result<bool, RuntimeStoreError> {
1740        let _ = runtime_id;
1741        Ok(false)
1742    }
1743
1744    /// Persist a single input state (for durable-before-ack).
1745    async fn persist_input_state(
1746        &self,
1747        runtime_id: &LogicalRuntimeId,
1748        state: &InputStatePersistenceRecord,
1749    ) -> Result<(), RuntimeStoreError>;
1750
1751    /// Load a single input state.
1752    async fn load_input_state(
1753        &self,
1754        runtime_id: &LogicalRuntimeId,
1755        input_id: &InputId,
1756    ) -> Result<Option<StoredInputState>, RuntimeStoreError>;
1757
1758    /// Load the last persisted machine lifecycle record bytes, if any.
1759    ///
1760    /// Implementations return only the opaque bytes previously obtained from
1761    /// [`MachineLifecycleCommit::store_record`]. The runtime crate decodes
1762    /// these bytes through `load_runtime_state` or internal recovery helpers;
1763    /// stores must not promote compatibility rows or bare runtime states into
1764    /// lifecycle authority.
1765    async fn load_machine_lifecycle_record(
1766        &self,
1767        runtime_id: &LogicalRuntimeId,
1768    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
1769
1770    /// Atomically commit machine-owned lifecycle state changes.
1771    ///
1772    /// Writes runtime state, generated runtime binding facts, and all input
1773    /// state updates in a single atomic operation. `MachineLifecycleCommit` has
1774    /// no public constructor, so this cannot be used by compatibility callers
1775    /// to pick runtime truth.
1776    async fn commit_machine_lifecycle(
1777        &self,
1778        runtime_id: &LogicalRuntimeId,
1779        commit: MachineLifecycleCommit,
1780        input_states: &[InputStatePersistenceRecord],
1781    ) -> Result<(), RuntimeStoreError>;
1782
1783    /// Atomically publish final unregister lifecycle truth and retire the
1784    /// matching ops-lifecycle epoch.
1785    ///
1786    /// The lifecycle record, input-state updates, and ops snapshot deletion
1787    /// MUST commit in one store transaction (or one indivisible in-memory
1788    /// critical section). A terminal lifecycle record with the old ops epoch
1789    /// still present is forbidden: recovery would otherwise resurrect stale
1790    /// operation/cursor authority after unregister. The commit also carries
1791    /// the exact retired ops epoch; implementations MUST atomically retain a
1792    /// durable deletion-wins fence for it, and every later
1793    /// `persist_ops_lifecycle` for that epoch must return
1794    /// [`RuntimeStoreError::OpsLifecycleEpochRetired`] rather than recreate the
1795    /// row. Implementations must also be idempotent so retry after a process
1796    /// crash following commit converges on the same terminal lifecycle with no
1797    /// ops snapshot and the same epoch fence.
1798    ///
1799    /// `Ok(())` means the whole finalization is visible. Every error except
1800    /// [`RuntimeStoreError::UnregisterFinalizationOutcomeUnknown`] MUST mean
1801    /// none of it is visible. A backend with an ambiguous commit
1802    /// acknowledgement must first resolve that ambiguity internally by
1803    /// reading its transaction authority. It may use the typed unknown error
1804    /// only when it cannot prove either the exact final state or the exact
1805    /// pre-transaction state; callers then retry without a durable rollback.
1806    async fn commit_unregister_finalization(
1807        &self,
1808        runtime_id: &LogicalRuntimeId,
1809        commit: MachineLifecycleCommit,
1810        input_states: &[InputStatePersistenceRecord],
1811    ) -> Result<(), RuntimeStoreError>;
1812
1813    /// Persist a snapshot of the ops lifecycle registry state.
1814    async fn persist_ops_lifecycle(
1815        &self,
1816        runtime_id: &LogicalRuntimeId,
1817        snapshot: &crate::ops_lifecycle::PersistedOpsSnapshot,
1818    ) -> Result<(), RuntimeStoreError> {
1819        let _ = (runtime_id, snapshot);
1820        Err(RuntimeStoreError::Unsupported(
1821            "persist_ops_lifecycle".into(),
1822        ))
1823    }
1824
1825    /// Load a previously persisted ops lifecycle snapshot.
1826    async fn load_ops_lifecycle(
1827        &self,
1828        runtime_id: &LogicalRuntimeId,
1829    ) -> Result<Option<crate::ops_lifecycle::PersistedOpsSnapshot>, RuntimeStoreError> {
1830        let _ = runtime_id;
1831        Err(RuntimeStoreError::Unsupported("load_ops_lifecycle".into()))
1832    }
1833
1834    /// Delete a previously persisted ops lifecycle snapshot.
1835    async fn delete_ops_lifecycle(
1836        &self,
1837        runtime_id: &LogicalRuntimeId,
1838    ) -> Result<(), RuntimeStoreError> {
1839        let _ = runtime_id;
1840        Err(RuntimeStoreError::Unsupported(
1841            "delete_ops_lifecycle".into(),
1842        ))
1843    }
1844}
1845
1846pub use memory::InMemoryRuntimeStore;
1847#[cfg(feature = "sqlite-store")]
1848pub use sqlite::SqliteRuntimeStore;
1849
1850#[cfg(test)]
1851mod lifecycle_record_compatibility_tests {
1852    use super::*;
1853
1854    fn operation_id(
1855        value: u128,
1856    ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
1857        meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId::from_uuid(
1858            uuid::Uuid::from_u128(value),
1859        )
1860    }
1861
1862    fn binding(seed: u8, name: &str, epoch: u64) -> SupervisorBindingReceipt {
1863        let pubkey = [seed; 32];
1864        SupervisorBindingReceipt::new(
1865            name.to_string(),
1866            meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey).as_str(),
1867            format!("inproc://{name}"),
1868            crate::comms_drain::encode_supervisor_signing_public_key(pubkey),
1869            epoch,
1870        )
1871    }
1872
1873    fn rotation(
1874        operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1875        phase: SupervisorRotationPersistencePhase,
1876        rejection: Option<SupervisorRotationRejection>,
1877        previous: SupervisorBindingReceipt,
1878        next: SupervisorBindingReceipt,
1879    ) -> SupervisorRotationReceipt {
1880        SupervisorRotationReceipt::new(operation_id, phase, rejection, previous, next)
1881    }
1882
1883    fn snapshot(authority: SupervisorAuthoritySnapshot) -> MachineLifecycleSnapshot {
1884        MachineLifecycleSnapshot::new(
1885            RuntimeState::Idle,
1886            MachineLifecycleBindingFacts::new(None, None, None, None),
1887            authority,
1888        )
1889    }
1890
1891    fn encode_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
1892        MachineLifecycleStoreRecord::from_snapshot(snapshot)
1893            .encode()
1894            .expect("encode lifecycle snapshot")
1895    }
1896
1897    fn encode_unvalidated_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
1898        serde_json::to_vec(&MachineLifecycleSnapshotStoreWire::from(snapshot))
1899            .expect("serialize deliberately corrupt lifecycle snapshot")
1900    }
1901
1902    fn encoded_value(snapshot: &MachineLifecycleSnapshot) -> serde_json::Value {
1903        serde_json::from_slice(&encode_snapshot(snapshot)).expect("decode encoded snapshot as JSON")
1904    }
1905
1906    fn assert_decode_fails(value: serde_json::Value) {
1907        let bytes = serde_json::to_vec(&value).expect("serialize corrupt lifecycle record");
1908        assert!(
1909            decode_machine_lifecycle_store_record(&bytes).is_err(),
1910            "corrupt lifecycle record must fail closed: {value}"
1911        );
1912    }
1913
1914    #[test]
1915    fn version_one_record_without_supervisor_authority_migrates_explicitly_to_unbound() {
1916        let bytes = serde_json::to_vec(&serde_json::json!({
1917            "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
1918            "runtime_state": RuntimeState::Retired,
1919            "binding": {
1920                "agent_runtime_id": "rt:session:legacy-v1",
1921                "fence_token": 19,
1922                "runtime_generation": 4,
1923                "runtime_epoch_id": "epoch-legacy-v1"
1924            }
1925        }))
1926        .expect("serialize legacy v1 lifecycle record");
1927
1928        let decoded = decode_machine_lifecycle_store_record(&bytes)
1929            .expect("valid v1 record without the additive field must decode");
1930        assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
1931        assert_eq!(
1932            decoded.supervisor_authority(),
1933            &SupervisorAuthoritySnapshot::UnboundNoReceipt
1934        );
1935    }
1936
1937    #[test]
1938    fn current_record_requires_supervisor_authority_and_unregister_progress_presence() {
1939        assert_decode_fails(serde_json::json!({
1940            "record_version": MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
1941            "runtime_state": RuntimeState::Idle,
1942            "binding": {
1943                "agent_runtime_id": null,
1944                "fence_token": null,
1945                "runtime_generation": null,
1946                "runtime_epoch_id": null
1947            },
1948            "unregister_progress": null
1949        }));
1950    }
1951
1952    #[test]
1953    fn current_nullable_fields_require_presence_but_accept_explicit_null() {
1954        let unbound = snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt);
1955        let encoded = encoded_value(&unbound);
1956        assert_eq!(
1957            decode_machine_lifecycle_store_record(
1958                &serde_json::to_vec(&encoded).expect("serialize valid current record")
1959            )
1960            .expect("explicit-null current binding fields must decode"),
1961            unbound
1962        );
1963        let mut missing_progress = encoded.clone();
1964        missing_progress
1965            .as_object_mut()
1966            .expect("lifecycle record object")
1967            .remove("unregister_progress");
1968        assert_decode_fails(missing_progress);
1969
1970        for field in [
1971            "agent_runtime_id",
1972            "fence_token",
1973            "runtime_generation",
1974            "runtime_epoch_id",
1975        ] {
1976            let mut partial = encoded.clone();
1977            partial["binding"]
1978                .as_object_mut()
1979                .expect("binding object")
1980                .remove(field);
1981            assert_decode_fails(partial);
1982        }
1983
1984        let completed = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
1985            operation_id(101),
1986            SupervisorRotationPersistencePhase::Completed,
1987            None,
1988            binding(30, "required-null-previous", 4),
1989            binding(31, "required-null-next", 5),
1990        )));
1991        let mut missing_rejection = encoded_value(&completed);
1992        assert!(missing_rejection["supervisor_authority"]["rotation"]["rejection"].is_null());
1993        missing_rejection["supervisor_authority"]["rotation"]
1994            .as_object_mut()
1995            .expect("rotation object")
1996            .remove("rejection");
1997        assert_decode_fails(missing_rejection);
1998    }
1999
2000    #[test]
2001    fn version_two_supervisor_record_migrates_with_no_unregister_progress() {
2002        let bytes = serde_json::to_vec(&serde_json::json!({
2003            "record_version": SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
2004            "runtime_state": RuntimeState::Retired,
2005            "binding": {
2006                "agent_runtime_id": "rt:session:legacy-v2",
2007                "fence_token": 23,
2008                "runtime_generation": 5,
2009                "runtime_epoch_id": "epoch-legacy-v2"
2010            },
2011            "supervisor_authority": { "kind": "unbound_no_receipt" }
2012        }))
2013        .expect("serialize v2 lifecycle record");
2014
2015        let decoded = decode_machine_lifecycle_store_record(&bytes)
2016            .expect("valid v2 supervisor record must migrate");
2017        assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
2018        assert_eq!(decoded.unregister_progress(), None);
2019    }
2020
2021    #[test]
2022    fn current_unregister_progress_rejects_forced_disposition_before_feedback() {
2023        let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt));
2024        value["unregister_progress"] = serde_json::json!({
2025            "runtime_loop_drain_pending": true,
2026            "comms_drain_exit_pending": false,
2027            "completion_waiter_drain_pending": true,
2028            "runtime_loop_forced_abort": true,
2029            "comms_drain_forced_abort": false
2030        });
2031        assert_decode_fails(value);
2032    }
2033
2034    #[test]
2035    fn version_one_migration_rejects_current_authority_fields() {
2036        assert_decode_fails(serde_json::json!({
2037            "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
2038            "runtime_state": RuntimeState::Idle,
2039            "binding": {
2040                "agent_runtime_id": null,
2041                "fence_token": null,
2042                "runtime_generation": null,
2043                "runtime_epoch_id": null
2044            },
2045            "supervisor_authority": { "kind": "unbound_no_receipt" }
2046        }));
2047    }
2048
2049    #[test]
2050    fn mixed_or_unknown_supervisor_authority_fields_fail_closed() {
2051        let current = binding(1, "current-supervisor", 7);
2052        let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::Bound(current)));
2053        value["supervisor_authority"]["rotation"] = serde_json::json!({});
2054        assert_decode_fails(value);
2055    }
2056
2057    #[test]
2058    fn completed_rotation_operation_receipt_round_trips_for_cold_observation() {
2059        let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2060            operation_id(1),
2061            SupervisorRotationPersistencePhase::Completed,
2062            None,
2063            binding(1, "previous-supervisor", 7),
2064            binding(2, "next-supervisor", 8),
2065        )));
2066
2067        let encoded = encode_snapshot(&snapshot);
2068        let decoded = decode_machine_lifecycle_store_record(&encoded)
2069            .expect("decode completed rotation receipt");
2070
2071        assert_eq!(decoded, snapshot);
2072    }
2073
2074    #[test]
2075    fn exact_current_completed_adoption_round_trips_but_other_equal_epoch_completion_fails() {
2076        let current = binding(3, "already-rotated-supervisor", 9);
2077        let adoption = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2078            operation_id(2),
2079            SupervisorRotationPersistencePhase::Completed,
2080            None,
2081            current.clone(),
2082            current,
2083        )));
2084        assert_eq!(
2085            decode_machine_lifecycle_store_record(&encode_snapshot(&adoption))
2086                .expect("exact-current legacy adoption receipt must decode"),
2087            adoption
2088        );
2089
2090        let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2091            operation_id(3),
2092            SupervisorRotationPersistencePhase::Completed,
2093            None,
2094            binding(3, "previous-supervisor", 9),
2095            binding(4, "different-supervisor", 9),
2096        )));
2097        assert!(
2098            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&non_advancing))
2099                .is_err()
2100        );
2101    }
2102
2103    #[test]
2104    fn malformed_rotation_descriptors_epochs_and_operation_ids_fail_closed() {
2105        let invalid_previous = SupervisorBindingReceipt::new(
2106            String::new(),
2107            "not-a-uuid".to_string(),
2108            "not-an-address".to_string(),
2109            "not-a-key".to_string(),
2110            1,
2111        );
2112        let invalid_previous_receipt =
2113            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2114                operation_id(4),
2115                SupervisorRotationPersistencePhase::Rejected,
2116                Some(SupervisorRotationRejection::InvalidTarget),
2117                invalid_previous,
2118                binding(5, "raw-target", 2),
2119            )));
2120        assert!(
2121            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
2122                &invalid_previous_receipt,
2123            ))
2124            .is_err()
2125        );
2126
2127        let invalid_next = SupervisorBindingReceipt::new(
2128            "invalid-target".to_string(),
2129            "not-a-uuid".to_string(),
2130            "not-an-address".to_string(),
2131            "not-a-key".to_string(),
2132            2,
2133        );
2134        let invalid_completed_target =
2135            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2136                operation_id(5),
2137                SupervisorRotationPersistencePhase::Completed,
2138                None,
2139                binding(6, "previous-supervisor", 1),
2140                invalid_next,
2141            )));
2142        assert!(
2143            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
2144                &invalid_completed_target,
2145            ))
2146            .is_err()
2147        );
2148
2149        let mut invalid_id = encoded_value(&snapshot(
2150            SupervisorAuthoritySnapshot::RotationOperation(rotation(
2151                operation_id(6),
2152                SupervisorRotationPersistencePhase::PreviousRevokePending,
2153                None,
2154                binding(7, "previous-supervisor", 1),
2155                binding(8, "next-supervisor", 2),
2156            )),
2157        ));
2158        invalid_id["supervisor_authority"]["rotation"]["operation_id"] =
2159            serde_json::json!("not-a-uuid");
2160        assert_decode_fails(invalid_id);
2161
2162        let nil_id = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2163            operation_id(0),
2164            SupervisorRotationPersistencePhase::PreviousRevokePending,
2165            None,
2166            binding(7, "previous-supervisor", 1),
2167            binding(8, "next-supervisor", 2),
2168        )));
2169        assert!(
2170            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&nil_id)).is_err()
2171        );
2172
2173        let non_advancing_pending =
2174            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2175                operation_id(13),
2176                SupervisorRotationPersistencePhase::PreviousRevokePending,
2177                None,
2178                binding(7, "previous-supervisor", 4),
2179                binding(8, "next-supervisor", 4),
2180            )));
2181        assert!(
2182            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
2183                &non_advancing_pending,
2184            ))
2185            .is_err()
2186        );
2187    }
2188
2189    #[test]
2190    fn rejected_invalid_or_unsupported_target_preserves_raw_evidence() {
2191        for (id, rejection) in [
2192            (7, SupervisorRotationRejection::InvalidTarget),
2193            (14, SupervisorRotationRejection::UnsupportedProtocolVersion),
2194        ] {
2195            let raw_invalid_target = SupervisorBindingReceipt::new(
2196                "".to_string(),
2197                "not-a-peer-id".to_string(),
2198                "not-an-address".to_string(),
2199                "not-a-signing-key".to_string(),
2200                0,
2201            );
2202            let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2203                operation_id(id),
2204                SupervisorRotationPersistencePhase::Rejected,
2205                Some(rejection),
2206                binding(9, "retained-supervisor", 11),
2207                raw_invalid_target,
2208            )));
2209            assert_eq!(
2210                decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
2211                    .expect("rejected raw target evidence must remain durable"),
2212                snapshot
2213            );
2214        }
2215    }
2216
2217    #[test]
2218    fn only_raw_target_rejections_are_durable_and_epoch_rejection_must_be_genuine() {
2219        for (id, rejection) in [
2220            (102, SupervisorRotationRejection::OperationConflict),
2221            (103, SupervisorRotationRejection::NotBound),
2222            (104, SupervisorRotationRejection::SenderMismatch),
2223        ] {
2224            let impossible = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2225                operation_id(id),
2226                SupervisorRotationPersistencePhase::Rejected,
2227                Some(rejection),
2228                binding(32, "retained-supervisor", 7),
2229                binding(33, "requested-supervisor", 8),
2230            )));
2231            assert!(
2232                MachineLifecycleStoreRecord::from_snapshot(&impossible)
2233                    .encode()
2234                    .is_err()
2235            );
2236            assert!(
2237                decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&impossible))
2238                    .is_err()
2239            );
2240        }
2241
2242        let advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2243            operation_id(105),
2244            SupervisorRotationPersistencePhase::Rejected,
2245            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
2246            binding(34, "retained-supervisor", 9),
2247            binding(35, "advancing-target", 10),
2248        )));
2249        assert!(
2250            MachineLifecycleStoreRecord::from_snapshot(&advancing)
2251                .encode()
2252                .is_err()
2253        );
2254        assert!(
2255            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&advancing))
2256                .is_err()
2257        );
2258
2259        let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
2260            operation_id(106),
2261            SupervisorRotationPersistencePhase::Rejected,
2262            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
2263            binding(36, "retained-supervisor", 11),
2264            binding(37, "non-advancing-target", 11),
2265        )));
2266        assert_eq!(
2267            decode_machine_lifecycle_store_record(&encode_snapshot(&non_advancing))
2268                .expect("genuine target-epoch rejection must remain durable"),
2269            non_advancing
2270        );
2271    }
2272
2273    #[test]
2274    fn malformed_current_authority_variants_fail_closed() {
2275        let malformed = SupervisorBindingReceipt::new(
2276            String::new(),
2277            "not-a-peer-id".to_string(),
2278            "not-an-address".to_string(),
2279            "not-a-signing-key".to_string(),
2280            1,
2281        );
2282        let bound = snapshot(SupervisorAuthoritySnapshot::Bound(malformed.clone()));
2283        assert!(
2284            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&bound)).is_err()
2285        );
2286
2287        let pending = snapshot(SupervisorAuthoritySnapshot::RevocationPending(
2288            SupervisorRevocationPendingReceipt::new(
2289                malformed.name().to_owned(),
2290                malformed.peer_id().to_owned(),
2291                malformed.address().to_owned(),
2292                malformed.signing_public_key().to_owned(),
2293                malformed.epoch(),
2294            ),
2295        ));
2296        assert!(
2297            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&pending)).is_err()
2298        );
2299
2300        let revoked = snapshot(SupervisorAuthoritySnapshot::RevokedReceipt(
2301            RevokedSupervisorReceipt::new(
2302                malformed.peer_id().to_owned(),
2303                malformed.signing_public_key().to_owned(),
2304                malformed.epoch(),
2305            ),
2306        ));
2307        assert!(
2308            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&revoked)).is_err()
2309        );
2310    }
2311
2312    #[test]
2313    fn partial_and_nonterminal_history_records_fail_closed() {
2314        let receipt = rotation(
2315            operation_id(8),
2316            SupervisorRotationPersistencePhase::Completed,
2317            None,
2318            binding(10, "history-previous", 1),
2319            binding(11, "history-next", 2),
2320        );
2321        let history = std::collections::BTreeMap::from([(receipt.operation_id(), receipt)]);
2322        let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2323            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2324                12,
2325                "current-supervisor",
2326                3,
2327            ))),
2328            terminal_receipts: history,
2329        });
2330
2331        let mut partial = encoded_value(&snapshot);
2332        partial["supervisor_authority"]["terminal_receipts"][0]
2333            .as_object_mut()
2334            .expect("history receipt object")
2335            .remove("next");
2336        assert_decode_fails(partial);
2337
2338        let mut nonterminal = encoded_value(&snapshot);
2339        nonterminal["supervisor_authority"]["terminal_receipts"][0]["phase"] =
2340            serde_json::json!("next_publish_pending");
2341        assert_decode_fails(nonterminal);
2342    }
2343
2344    #[test]
2345    fn duplicate_nested_and_active_history_conflicts_fail_closed() {
2346        let history_receipt = rotation(
2347            operation_id(9),
2348            SupervisorRotationPersistencePhase::Completed,
2349            None,
2350            binding(13, "history-previous", 1),
2351            binding(14, "history-next", 2),
2352        );
2353        let history = std::collections::BTreeMap::from([(
2354            history_receipt.operation_id(),
2355            history_receipt.clone(),
2356        )]);
2357        let wrapper = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2358            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2359                15,
2360                "current-supervisor",
2361                3,
2362            ))),
2363            terminal_receipts: history,
2364        });
2365
2366        let mut duplicate = encoded_value(&wrapper);
2367        let receipt = duplicate["supervisor_authority"]["terminal_receipts"][0].clone();
2368        duplicate["supervisor_authority"]["terminal_receipts"]
2369            .as_array_mut()
2370            .expect("history receipt array")
2371            .push(receipt);
2372        assert_decode_fails(duplicate);
2373
2374        let mut nested = encoded_value(&wrapper);
2375        let nested_current = nested["supervisor_authority"].clone();
2376        nested["supervisor_authority"]["current"] = nested_current;
2377        assert_decode_fails(nested);
2378
2379        let active_conflict = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2380            current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(
2381                history_receipt.clone(),
2382            )),
2383            terminal_receipts: std::collections::BTreeMap::from([(
2384                history_receipt.operation_id(),
2385                history_receipt,
2386            )]),
2387        });
2388        assert!(
2389            MachineLifecycleStoreRecord::from_snapshot(&active_conflict)
2390                .encode()
2391                .is_err()
2392        );
2393
2394        let empty_history = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2395            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2396                20,
2397                "current-supervisor",
2398                4,
2399            ))),
2400            terminal_receipts: std::collections::BTreeMap::new(),
2401        });
2402        assert!(
2403            MachineLifecycleStoreRecord::from_snapshot(&empty_history)
2404                .encode()
2405                .is_err()
2406        );
2407        assert!(
2408            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&empty_history))
2409                .is_err()
2410        );
2411
2412        let mismatched_key = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2413            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2414                21,
2415                "current-supervisor",
2416                4,
2417            ))),
2418            terminal_receipts: std::collections::BTreeMap::from([(
2419                operation_id(99),
2420                rotation(
2421                    operation_id(98),
2422                    SupervisorRotationPersistencePhase::Completed,
2423                    None,
2424                    binding(22, "history-previous", 2),
2425                    binding(23, "history-next", 3),
2426                ),
2427            )]),
2428        });
2429        assert!(
2430            MachineLifecycleStoreRecord::from_snapshot(&mismatched_key)
2431                .encode()
2432                .is_err()
2433        );
2434    }
2435
2436    #[test]
2437    fn history_current_epoch_and_same_epoch_identity_must_cohere() {
2438        let previous = binding(38, "history-previous", 12);
2439        let next = binding(39, "history-next", 13);
2440        let completed = rotation(
2441            operation_id(107),
2442            SupervisorRotationPersistencePhase::Completed,
2443            None,
2444            previous.clone(),
2445            next.clone(),
2446        );
2447        let history =
2448            std::collections::BTreeMap::from([(completed.operation_id(), completed.clone())]);
2449
2450        let stale_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2451            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2452                38,
2453                "refreshed-history-previous",
2454                12,
2455            ))),
2456            terminal_receipts: history.clone(),
2457        });
2458        assert!(
2459            MachineLifecycleStoreRecord::from_snapshot(&stale_current)
2460                .encode()
2461                .is_err()
2462        );
2463        assert!(
2464            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&stale_current))
2465                .is_err()
2466        );
2467
2468        let conflicting_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2469            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2470                40,
2471                "conflicting-current",
2472                13,
2473            ))),
2474            terminal_receipts: history.clone(),
2475        });
2476        assert!(
2477            MachineLifecycleStoreRecord::from_snapshot(&conflicting_current)
2478                .encode()
2479                .is_err()
2480        );
2481        assert!(
2482            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
2483                &conflicting_current,
2484            ))
2485            .is_err()
2486        );
2487
2488        let route_refreshed_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2489            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
2490                39,
2491                "route-refreshed-history-next",
2492                13,
2493            ))),
2494            terminal_receipts: history,
2495        });
2496        assert_eq!(
2497            decode_machine_lifecycle_store_record(&encode_snapshot(&route_refreshed_current))
2498                .expect("same identity may refresh route metadata within one epoch"),
2499            route_refreshed_current
2500        );
2501    }
2502
2503    #[test]
2504    fn terminal_history_survives_later_rotation_and_recovery() {
2505        let first = rotation(
2506            operation_id(10),
2507            SupervisorRotationPersistencePhase::Completed,
2508            None,
2509            binding(16, "first-supervisor", 1),
2510            binding(17, "second-supervisor", 2),
2511        );
2512        let rejected = rotation(
2513            operation_id(11),
2514            SupervisorRotationPersistencePhase::Rejected,
2515            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
2516            binding(17, "second-supervisor", 2),
2517            binding(18, "rejected-supervisor", 2),
2518        );
2519        let later = rotation(
2520            operation_id(12),
2521            SupervisorRotationPersistencePhase::Completed,
2522            None,
2523            binding(17, "second-supervisor", 2),
2524            binding(19, "current-supervisor", 3),
2525        );
2526        let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
2527            current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(later)),
2528            terminal_receipts: std::collections::BTreeMap::from([
2529                (first.operation_id(), first),
2530                (rejected.operation_id(), rejected),
2531            ]),
2532        });
2533
2534        let decoded = decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
2535            .expect("later rotation and old terminal history must recover together");
2536        assert_eq!(decoded, snapshot);
2537    }
2538}