1pub mod memory;
7#[cfg(feature = "sqlite-store")]
8pub mod sqlite;
9
10use std::collections::{HashMap, HashSet};
11
12use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
13use sha2::{Digest, Sha256};
14
15use crate::identifiers::LogicalRuntimeId;
16use crate::input_state::{InputStatePersistenceRecord, StoredInputState};
17use crate::runtime_state::RuntimeState;
18
19const LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 1;
20const SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 2;
21const UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 3;
22pub(crate) const MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 4;
23
24pub const MAX_INPUT_STATE_BATCH_CAS: usize = 256;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InputStateBatchCasOutcome {
32 Swapped,
36 Stale,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum FencedInputStateBatchCasOutcome {
45 Swapped,
48 Stale,
51 FenceConflict { reason: String },
53 FenceBackoff { reason: String },
56}
57
58#[derive(Debug)]
59struct PreparedInputStateBatchCasRow {
60 input_id: InputId,
61 expected_json: Vec<u8>,
62 replacement: StoredInputState,
63 #[cfg_attr(not(feature = "sqlite-store"), allow(dead_code))]
66 replacement_json: Vec<u8>,
67}
68
69fn prepare_input_state_batch_cas(
70 expected: &[StoredInputState],
71 replacements: &[InputStatePersistenceRecord],
72) -> Result<Vec<PreparedInputStateBatchCasRow>, RuntimeStoreError> {
73 if expected.len() != replacements.len() {
74 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
75 reason: format!(
76 "expected row count {} does not match replacement row count {}",
77 expected.len(),
78 replacements.len()
79 ),
80 });
81 }
82 if expected.len() > MAX_INPUT_STATE_BATCH_CAS {
83 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
84 reason: format!(
85 "batch contains {} rows, exceeding the maximum of {MAX_INPUT_STATE_BATCH_CAS}",
86 expected.len()
87 ),
88 });
89 }
90
91 let mut expected_ids = HashSet::with_capacity(expected.len());
92 for row in expected {
93 if !expected_ids.insert(row.state.input_id.clone()) {
94 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
95 reason: format!("expected batch repeats input {}", row.state.input_id),
96 });
97 }
98 }
99
100 let mut replacement_by_id = HashMap::with_capacity(replacements.len());
101 for record in replacements {
102 let replacement = record.clone_stored();
103 let input_id = replacement.state.input_id.clone();
104 let replacement_json = serde_json::to_vec(&replacement)
105 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
106 if replacement_by_id
107 .insert(input_id.clone(), (replacement, replacement_json))
108 .is_some()
109 {
110 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
111 reason: format!("replacement batch repeats input {input_id}"),
112 });
113 }
114 }
115
116 let mut prepared = Vec::with_capacity(expected.len());
117 for expected_row in expected {
118 let input_id = expected_row.state.input_id.clone();
119 let Some((replacement, replacement_json)) = replacement_by_id.remove(&input_id) else {
120 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
121 reason: format!("replacement batch does not contain expected input {input_id}"),
122 });
123 };
124 let expected_json = serde_json::to_vec(expected_row)
125 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
126 prepared.push(PreparedInputStateBatchCasRow {
127 input_id,
128 expected_json,
129 replacement,
130 replacement_json,
131 });
132 }
133 if let Some(extra) = replacement_by_id.keys().next() {
134 return Err(RuntimeStoreError::InvalidInputStateBatchCas {
135 reason: format!("replacement batch contains unexpected input {extra}"),
136 });
137 }
138 Ok(prepared)
139}
140
141#[derive(Debug, Clone, thiserror::Error)]
143#[non_exhaustive]
144pub enum RuntimeStoreError {
145 #[error("Store write failed: {0}")]
147 WriteFailed(String),
148 #[error("Store read failed: {0}")]
150 ReadFailed(String),
151 #[error("Session store key mismatch: expected {expected}, actual {actual}")]
153 SessionKeyMismatch {
154 expected: meerkat_core::types::SessionId,
155 actual: meerkat_core::types::SessionId,
156 },
157 #[error("Not found: {0}")]
159 NotFound(String),
160 #[error("Unsupported store operation: {0}")]
162 Unsupported(String),
163 #[error("Ops lifecycle epoch {epoch_id} for runtime {runtime_id} is retired")]
166 OpsLifecycleEpochRetired {
167 runtime_id: String,
168 epoch_id: meerkat_core::RuntimeEpochId,
169 },
170 #[error("Unregister finalization outcome is unknown: {0}")]
176 UnregisterFinalizationOutcomeUnknown(String),
177 #[error("Transcript revision conflict: expected {expected}, actual {actual}")]
179 TranscriptRevisionConflict { expected: String, actual: String },
180 #[error("Session snapshot for runtime '{runtime_id}' was superseded by the durable head")]
184 SessionSnapshotSuperseded { runtime_id: String },
185 #[error("Invalid input-state batch compare-and-swap: {reason}")]
187 InvalidInputStateBatchCas { reason: String },
188 #[error("Machine lifecycle repair is blocked: {detail}")]
195 MachineLifecycleRepairBlocked {
196 evidence_digest: Option<String>,
197 detail: String,
198 },
199 #[error(
203 "schema for domain '{domain}' is from the future: file has version {found}, \
204 this binary supports up to {supported}"
205 )]
206 SchemaFromTheFuture {
207 domain: String,
208 found: i64,
209 supported: i64,
210 },
211 #[error("maintenance fence is held for '{path}'; storage is under offline maintenance")]
214 MaintenanceFenceHeld { path: String },
215 #[error("Internal error: {0}")]
217 Internal(String),
218}
219
220pub type AuthOAuthFlowSnapshotUpdate<'a> =
222 dyn FnMut(Option<&[u8]>) -> Result<Vec<u8>, RuntimeStoreError> + 'a;
223
224#[derive(Debug, Clone)]
226pub struct SessionDelta {
227 pub session_snapshot: Vec<u8>,
229}
230
231fn validated_compaction_projection_intents(
232 session: &meerkat_core::Session,
233) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
234 session
235 .validated_compaction_projection_intents()
236 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))
237}
238
239pub(crate) fn complete_compaction_projection_checkpoint(
247 session: &mut meerkat_core::Session,
248 projection: &meerkat_core::CompactionProjectionId,
249) -> Result<(), RuntimeStoreError> {
250 let predecessor = match session
251 .try_checkpoint_state()
252 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?
253 {
254 meerkat_core::SessionCheckpointState::Verified(stamp) => Some(stamp),
255 meerkat_core::SessionCheckpointState::LegacyUnverified { .. } => None,
256 };
257
258 let completed = session
259 .complete_compaction_projection_intent(projection)
260 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
261
262 if completed.is_none() {
263 return Ok(());
264 }
265
266 if let Some(predecessor) = predecessor {
267 let successor = meerkat_core::SessionCheckpointStamp::successor(
268 session,
269 &predecessor,
270 meerkat_core::SessionCheckpointProvenance::RunBoundaryCommit,
271 )
272 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
273 session
274 .install_checkpoint_stamp(successor)
275 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
276 }
277
278 Ok(())
279}
280
281#[derive(Debug, Clone, Default, PartialEq, Eq)]
288pub struct MachineLifecycleBindingFacts {
289 agent_runtime_id: Option<String>,
290 fence_token: Option<u64>,
291 runtime_generation: Option<u64>,
292 runtime_epoch_id: Option<String>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct RevokedSupervisorReceipt {
303 peer_id: String,
304 signing_public_key: String,
305 epoch: u64,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct SupervisorBindingReceipt {
312 name: String,
313 peer_id: String,
314 address: String,
315 signing_public_key: String,
316 epoch: u64,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct SupervisorRevocationPendingReceipt {
327 name: String,
328 peer_id: String,
329 address: String,
330 signing_public_key: String,
331 epoch: u64,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
335#[serde(rename_all = "snake_case")]
336pub enum SupervisorRotationPersistencePhase {
337 PreviousRevokePending,
338 NextPublishPending,
339 Completed,
340 Rejected,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
344#[serde(rename_all = "snake_case")]
345pub enum SupervisorRotationRejection {
346 OperationConflict,
347 NotBound,
348 SenderMismatch,
349 TargetEpochNotAdvanced,
350 InvalidTarget,
351 UnsupportedProtocolVersion,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub struct SupervisorRotationReceipt {
356 operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
357 phase: SupervisorRotationPersistencePhase,
358 rejection: Option<SupervisorRotationRejection>,
359 previous: SupervisorBindingReceipt,
360 next: SupervisorBindingReceipt,
361}
362
363impl SupervisorRotationReceipt {
364 pub(crate) fn new(
365 operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
366 phase: SupervisorRotationPersistencePhase,
367 rejection: Option<SupervisorRotationRejection>,
368 previous: SupervisorBindingReceipt,
369 next: SupervisorBindingReceipt,
370 ) -> Self {
371 Self {
372 operation_id,
373 phase,
374 rejection,
375 previous,
376 next,
377 }
378 }
379
380 pub fn operation_id(
381 &self,
382 ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
383 self.operation_id
384 }
385
386 pub fn phase(&self) -> SupervisorRotationPersistencePhase {
387 self.phase
388 }
389
390 pub fn rejection(&self) -> Option<SupervisorRotationRejection> {
391 self.rejection
392 }
393
394 pub fn previous(&self) -> &SupervisorBindingReceipt {
395 &self.previous
396 }
397
398 pub fn next(&self) -> &SupervisorBindingReceipt {
399 &self.next
400 }
401}
402
403impl SupervisorBindingReceipt {
404 pub(crate) fn new(
405 name: String,
406 peer_id: String,
407 address: String,
408 signing_public_key: String,
409 epoch: u64,
410 ) -> Self {
411 Self {
412 name,
413 peer_id,
414 address,
415 signing_public_key,
416 epoch,
417 }
418 }
419
420 pub fn name(&self) -> &str {
421 &self.name
422 }
423
424 pub fn peer_id(&self) -> &str {
425 &self.peer_id
426 }
427
428 pub fn address(&self) -> &str {
429 &self.address
430 }
431
432 pub fn signing_public_key(&self) -> &str {
433 &self.signing_public_key
434 }
435
436 pub fn epoch(&self) -> u64 {
437 self.epoch
438 }
439}
440
441impl RevokedSupervisorReceipt {
442 pub(crate) fn new(peer_id: String, signing_public_key: String, epoch: u64) -> Self {
443 Self {
444 peer_id,
445 signing_public_key,
446 epoch,
447 }
448 }
449
450 pub fn peer_id(&self) -> &str {
451 &self.peer_id
452 }
453
454 pub fn signing_public_key(&self) -> &str {
455 &self.signing_public_key
456 }
457
458 pub fn epoch(&self) -> u64 {
459 self.epoch
460 }
461}
462
463impl SupervisorRevocationPendingReceipt {
464 pub(crate) fn new(
465 name: String,
466 peer_id: String,
467 address: String,
468 signing_public_key: String,
469 epoch: u64,
470 ) -> Self {
471 Self {
472 name,
473 peer_id,
474 address,
475 signing_public_key,
476 epoch,
477 }
478 }
479
480 pub fn name(&self) -> &str {
481 &self.name
482 }
483
484 pub fn peer_id(&self) -> &str {
485 &self.peer_id
486 }
487
488 pub fn address(&self) -> &str {
489 &self.address
490 }
491
492 pub fn signing_public_key(&self) -> &str {
493 &self.signing_public_key
494 }
495
496 pub fn epoch(&self) -> u64 {
497 self.epoch
498 }
499}
500
501#[derive(Debug, Clone, Default, PartialEq, Eq)]
505pub enum SupervisorAuthoritySnapshot {
506 #[default]
507 UnboundNoReceipt,
508 Bound(SupervisorBindingReceipt),
509 RevocationPending(SupervisorRevocationPendingReceipt),
510 RotationOperation(SupervisorRotationReceipt),
511 RevokedReceipt(RevokedSupervisorReceipt),
512 WithRotationHistory {
513 current: Box<SupervisorAuthoritySnapshot>,
514 terminal_receipts: std::collections::BTreeMap<
515 meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
516 SupervisorRotationReceipt,
517 >,
518 },
519}
520
521impl MachineLifecycleBindingFacts {
522 pub(crate) fn new(
523 agent_runtime_id: Option<String>,
524 fence_token: Option<u64>,
525 runtime_generation: Option<u64>,
526 runtime_epoch_id: Option<String>,
527 ) -> Self {
528 Self {
529 agent_runtime_id,
530 fence_token,
531 runtime_generation,
532 runtime_epoch_id,
533 }
534 }
535
536 pub fn agent_runtime_id(&self) -> Option<&str> {
537 self.agent_runtime_id.as_deref()
538 }
539
540 pub fn fence_token(&self) -> Option<u64> {
541 self.fence_token
542 }
543
544 pub fn runtime_generation(&self) -> Option<u64> {
545 self.runtime_generation
546 }
547
548 pub fn runtime_epoch_id(&self) -> Option<&str> {
549 self.runtime_epoch_id.as_deref()
550 }
551}
552
553#[derive(Debug, Clone, PartialEq, Eq, Hash)]
559pub struct MachineLifecycleObservationVersion(String);
560
561impl MachineLifecycleObservationVersion {
562 pub fn from_raw_record(bytes: &[u8]) -> Self {
568 Self(format!("sha256:{:x}", Sha256::digest(bytes)))
569 }
570
571 #[must_use]
572 pub fn as_str(&self) -> &str {
573 &self.0
574 }
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
583#[serde(rename_all = "snake_case")]
584pub enum MachineLifecyclePreRunPhase {
585 Idle,
586 Attached,
587 Retired,
588}
589
590#[derive(Debug, Clone, Default, PartialEq, Eq)]
591pub struct MachineLifecycleRunFacts {
592 current_run_id: Option<RunId>,
593 pre_run_phase: Option<MachineLifecyclePreRunPhase>,
594}
595
596impl MachineLifecycleRunFacts {
597 pub(crate) fn new(
598 current_run_id: Option<RunId>,
599 pre_run_phase: Option<MachineLifecyclePreRunPhase>,
600 ) -> Self {
601 Self {
602 current_run_id,
603 pre_run_phase,
604 }
605 }
606
607 #[must_use]
608 pub fn current_run_id(&self) -> Option<&RunId> {
609 self.current_run_id.as_ref()
610 }
611
612 #[must_use]
613 pub fn pre_run_phase(&self) -> Option<MachineLifecyclePreRunPhase> {
614 self.pre_run_phase
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
625pub struct DecodedMachineLifecycleObservation {
626 record_version: u16,
627 runtime_state: Option<RuntimeState>,
628 binding: MachineLifecycleBindingFacts,
629 run: MachineLifecycleRunFacts,
630 supervisor_authority: SupervisorAuthoritySnapshot,
631 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
632}
633
634impl DecodedMachineLifecycleObservation {
635 #[must_use]
636 pub fn record_version(&self) -> u16 {
637 self.record_version
638 }
639
640 #[must_use]
641 pub fn runtime_state(&self) -> Option<RuntimeState> {
642 self.runtime_state
643 }
644
645 #[must_use]
646 pub fn binding(&self) -> &MachineLifecycleBindingFacts {
647 &self.binding
648 }
649
650 #[must_use]
651 pub fn run(&self) -> &MachineLifecycleRunFacts {
652 &self.run
653 }
654
655 #[must_use]
656 pub fn supervisor_authority(&self) -> &SupervisorAuthoritySnapshot {
657 &self.supervisor_authority
658 }
659
660 #[must_use]
661 pub fn unregister_progress(&self) -> Option<&MachineUnregisterProgressSnapshot> {
662 self.unregister_progress.as_ref()
663 }
664}
665
666#[derive(Debug, Clone, PartialEq, Eq)]
672pub enum MachineLifecycleObservation {
673 Missing,
674 Decoded {
675 record: DecodedMachineLifecycleObservation,
676 version: MachineLifecycleObservationVersion,
677 },
678 Unsupported {
679 record_version: u64,
680 evidence_digest: String,
681 version: MachineLifecycleObservationVersion,
682 },
683 Malformed {
684 record_version: Option<u64>,
685 evidence_digest: String,
686 version: MachineLifecycleObservationVersion,
687 detail: String,
688 },
689}
690
691impl MachineLifecycleObservation {
692 #[must_use]
698 pub fn from_raw_record(bytes: &[u8]) -> Self {
699 classify_machine_lifecycle_record(bytes)
700 }
701
702 #[must_use]
703 pub fn version(&self) -> Option<&MachineLifecycleObservationVersion> {
704 match self {
705 Self::Missing => None,
706 Self::Decoded { version, .. }
707 | Self::Unsupported { version, .. }
708 | Self::Malformed { version, .. } => Some(version),
709 }
710 }
711
712 #[must_use]
713 pub fn evidence_digest(&self) -> Option<&str> {
714 match self {
715 Self::Unsupported {
716 evidence_digest, ..
717 }
718 | Self::Malformed {
719 evidence_digest, ..
720 } => Some(evidence_digest),
721 Self::Missing | Self::Decoded { .. } => None,
722 }
723 }
724}
725
726#[derive(Debug, Clone, PartialEq, Eq)]
728pub enum MachineLifecycleExpectedVersion {
729 Missing,
730 Version(MachineLifecycleObservationVersion),
731}
732
733impl MachineLifecycleObservation {
734 #[must_use]
740 pub fn expected_version(&self) -> MachineLifecycleExpectedVersion {
741 self.version()
742 .map_or(MachineLifecycleExpectedVersion::Missing, |version| {
743 MachineLifecycleExpectedVersion::Version(version.clone())
744 })
745 }
746}
747
748#[derive(Debug, Clone, PartialEq, Eq)]
754pub enum RuntimeStoreWriteFenceOutcome {
755 Applied,
757 Conflict { reason: String },
759 Backoff { reason: String },
762}
763
764pub trait RuntimeStoreWriteFence: Send + Sync {
777 fn execute_if_current(
778 &self,
779 operation: Box<dyn FnOnce() -> Result<(), RuntimeStoreError> + '_>,
780 ) -> Result<RuntimeStoreWriteFenceOutcome, RuntimeStoreError>;
781}
782
783pub(crate) fn execute_runtime_store_write_fence(
784 write_fence: &dyn RuntimeStoreWriteFence,
785 operation: impl FnOnce() -> Result<(), RuntimeStoreError>,
786) -> Result<RuntimeStoreWriteFenceOutcome, RuntimeStoreError> {
787 let invoked = std::cell::Cell::new(false);
788 let operation_result = std::cell::RefCell::new(None);
789 let checked_operation = || {
790 invoked.set(true);
791 let result = operation();
792 *operation_result.borrow_mut() = Some(result.clone());
793 result
794 };
795 let outcome = write_fence.execute_if_current(Box::new(checked_operation))?;
796 if let Some(Err(error)) = operation_result.borrow_mut().take() {
797 return Err(error);
798 }
799 let shape_is_valid = matches!(
800 (&outcome, invoked.get()),
801 (RuntimeStoreWriteFenceOutcome::Applied, true)
802 | (
803 RuntimeStoreWriteFenceOutcome::Conflict { .. }
804 | RuntimeStoreWriteFenceOutcome::Backoff { .. },
805 false,
806 )
807 );
808 if !shape_is_valid {
809 return Err(RuntimeStoreError::Internal(
810 "runtime write fence returned an outcome inconsistent with operation execution"
811 .to_string(),
812 ));
813 }
814 Ok(outcome)
815}
816
817#[derive(Debug, Clone, PartialEq, Eq)]
823pub enum FencedMachineLifecycleCasOutcome {
824 Applied {
825 record: DecodedMachineLifecycleObservation,
826 version: MachineLifecycleObservationVersion,
827 },
828 AlreadyExact {
829 record: DecodedMachineLifecycleObservation,
830 version: MachineLifecycleObservationVersion,
831 },
832 Conflict {
833 current: MachineLifecycleObservation,
834 },
835 FenceConflict {
836 reason: String,
837 },
838 FenceBackoff {
839 reason: String,
840 },
841}
842
843#[derive(Debug, Clone, PartialEq, Eq)]
845pub enum MachineLifecycleCasOutcome {
846 Applied {
847 version: MachineLifecycleObservationVersion,
848 },
849 Conflict {
850 current: MachineLifecycleObservation,
851 },
852}
853
854#[derive(Debug, Clone, PartialEq, Eq)]
856pub struct MachineLifecycleSnapshot {
857 runtime_state: RuntimeState,
858 binding: MachineLifecycleBindingFacts,
859 run: MachineLifecycleRunFacts,
860 supervisor_authority: SupervisorAuthoritySnapshot,
861 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
862}
863
864#[derive(Debug, Clone, PartialEq, Eq)]
868pub struct MachineUnregisterProgressSnapshot {
869 runtime_loop_drain_pending: bool,
870 comms_drain_exit_pending: bool,
871 completion_waiter_drain_pending: bool,
872 runtime_loop_forced_abort: bool,
873 comms_drain_forced_abort: bool,
874}
875
876impl MachineUnregisterProgressSnapshot {
877 pub(crate) fn new(
878 runtime_loop_drain_pending: bool,
879 comms_drain_exit_pending: bool,
880 completion_waiter_drain_pending: bool,
881 runtime_loop_forced_abort: bool,
882 comms_drain_forced_abort: bool,
883 ) -> Self {
884 Self {
885 runtime_loop_drain_pending,
886 comms_drain_exit_pending,
887 completion_waiter_drain_pending,
888 runtime_loop_forced_abort,
889 comms_drain_forced_abort,
890 }
891 }
892
893 pub(crate) fn runtime_loop_drain_pending(&self) -> bool {
894 self.runtime_loop_drain_pending
895 }
896
897 pub(crate) fn comms_drain_exit_pending(&self) -> bool {
898 self.comms_drain_exit_pending
899 }
900
901 pub(crate) fn completion_waiter_drain_pending(&self) -> bool {
902 self.completion_waiter_drain_pending
903 }
904
905 pub(crate) fn runtime_loop_forced_abort(&self) -> bool {
906 self.runtime_loop_forced_abort
907 }
908
909 pub(crate) fn comms_drain_forced_abort(&self) -> bool {
910 self.comms_drain_forced_abort
911 }
912}
913
914impl MachineLifecycleSnapshot {
915 pub(crate) fn new(
916 runtime_state: RuntimeState,
917 binding: MachineLifecycleBindingFacts,
918 supervisor_authority: SupervisorAuthoritySnapshot,
919 ) -> Self {
920 Self::new_with_unregister_progress(runtime_state, binding, supervisor_authority, None)
921 }
922
923 pub(crate) fn new_with_unregister_progress(
924 runtime_state: RuntimeState,
925 binding: MachineLifecycleBindingFacts,
926 supervisor_authority: SupervisorAuthoritySnapshot,
927 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
928 ) -> Self {
929 Self::new_with_run_and_unregister_progress(
930 runtime_state,
931 binding,
932 MachineLifecycleRunFacts::default(),
933 supervisor_authority,
934 unregister_progress,
935 )
936 }
937
938 pub(crate) fn new_with_run_and_unregister_progress(
939 runtime_state: RuntimeState,
940 binding: MachineLifecycleBindingFacts,
941 run: MachineLifecycleRunFacts,
942 supervisor_authority: SupervisorAuthoritySnapshot,
943 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
944 ) -> Self {
945 Self {
946 runtime_state,
947 binding,
948 run,
949 supervisor_authority,
950 unregister_progress,
951 }
952 }
953
954 pub fn runtime_state(&self) -> RuntimeState {
956 self.runtime_state
957 }
958
959 pub fn binding(&self) -> &MachineLifecycleBindingFacts {
961 &self.binding
962 }
963
964 pub fn run(&self) -> &MachineLifecycleRunFacts {
966 &self.run
967 }
968
969 pub fn supervisor_authority(&self) -> &SupervisorAuthoritySnapshot {
970 &self.supervisor_authority
971 }
972
973 pub fn unregister_progress(&self) -> Option<&MachineUnregisterProgressSnapshot> {
974 self.unregister_progress.as_ref()
975 }
976}
977
978#[allow(
979 clippy::option_option,
980 reason = "serde distinguishes missing from explicit null"
981)]
982fn deserialize_present_nullable<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
983where
984 D: serde::Deserializer<'de>,
985 T: serde::Deserialize<'de>,
986{
987 <Option<T> as serde::Deserialize>::deserialize(deserializer).map(Some)
988}
989
990#[allow(
991 clippy::option_option,
992 reason = "serde distinguishes missing from explicit null"
993)]
994fn require_present_nullable<T>(
995 value: Option<Option<T>>,
996 field: &str,
997) -> Result<Option<T>, RuntimeStoreError> {
998 value.ok_or_else(|| {
999 RuntimeStoreError::ReadFailed(format!(
1000 "machine lifecycle field {field} is required (explicit null is allowed)"
1001 ))
1002 })
1003}
1004
1005#[derive(serde::Serialize, serde::Deserialize)]
1006#[serde(deny_unknown_fields)]
1007struct MachineLifecycleBindingFactsStoreWire {
1008 #[allow(
1009 clippy::option_option,
1010 reason = "serde distinguishes missing from explicit null"
1011 )]
1012 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1013 agent_runtime_id: Option<Option<String>>,
1014 #[allow(
1015 clippy::option_option,
1016 reason = "serde distinguishes missing from explicit null"
1017 )]
1018 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1019 fence_token: Option<Option<u64>>,
1020 #[allow(
1021 clippy::option_option,
1022 reason = "serde distinguishes missing from explicit null"
1023 )]
1024 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1025 runtime_generation: Option<Option<u64>>,
1026 #[allow(
1027 clippy::option_option,
1028 reason = "serde distinguishes missing from explicit null"
1029 )]
1030 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1031 runtime_epoch_id: Option<Option<String>>,
1032}
1033
1034#[derive(serde::Deserialize)]
1035#[serde(deny_unknown_fields)]
1036struct MachineLifecycleBindingFactsStoreWireV1 {
1037 agent_runtime_id: Option<String>,
1038 fence_token: Option<u64>,
1039 runtime_generation: Option<u64>,
1040 runtime_epoch_id: Option<String>,
1041}
1042
1043impl From<&MachineLifecycleBindingFacts> for MachineLifecycleBindingFactsStoreWire {
1044 fn from(binding: &MachineLifecycleBindingFacts) -> Self {
1045 Self {
1046 agent_runtime_id: Some(binding.agent_runtime_id().map(ToOwned::to_owned)),
1047 fence_token: Some(binding.fence_token()),
1048 runtime_generation: Some(binding.runtime_generation()),
1049 runtime_epoch_id: Some(binding.runtime_epoch_id().map(ToOwned::to_owned)),
1050 }
1051 }
1052}
1053
1054impl TryFrom<MachineLifecycleBindingFactsStoreWire> for MachineLifecycleBindingFacts {
1055 type Error = RuntimeStoreError;
1056
1057 fn try_from(binding: MachineLifecycleBindingFactsStoreWire) -> Result<Self, Self::Error> {
1058 Ok(Self::new(
1059 require_present_nullable(binding.agent_runtime_id, "binding.agent_runtime_id")?,
1060 require_present_nullable(binding.fence_token, "binding.fence_token")?,
1061 require_present_nullable(binding.runtime_generation, "binding.runtime_generation")?,
1062 require_present_nullable(binding.runtime_epoch_id, "binding.runtime_epoch_id")?,
1063 ))
1064 }
1065}
1066
1067impl From<MachineLifecycleBindingFactsStoreWireV1> for MachineLifecycleBindingFacts {
1068 fn from(binding: MachineLifecycleBindingFactsStoreWireV1) -> Self {
1069 Self::new(
1070 binding.agent_runtime_id,
1071 binding.fence_token,
1072 binding.runtime_generation,
1073 binding.runtime_epoch_id,
1074 )
1075 }
1076}
1077
1078#[derive(serde::Serialize)]
1079#[serde(deny_unknown_fields)]
1080struct MachineLifecycleSnapshotStoreWire {
1081 record_version: u16,
1082 runtime_state: RuntimeState,
1083 binding: MachineLifecycleBindingFactsStoreWire,
1084 current_run_id: Option<RunId>,
1085 pre_run_phase: Option<MachineLifecyclePreRunPhase>,
1086 supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1087 unregister_progress: Option<MachineUnregisterProgressSnapshotStoreWire>,
1088}
1089
1090#[derive(serde::Deserialize)]
1091#[serde(deny_unknown_fields)]
1092struct MachineLifecycleObservationStoreWireV4 {
1093 record_version: u16,
1094 #[allow(
1095 clippy::option_option,
1096 reason = "serde distinguishes a missing phase from an explicitly absent observed phase"
1097 )]
1098 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1099 runtime_state: Option<Option<RuntimeState>>,
1100 binding: MachineLifecycleBindingFactsStoreWire,
1101 #[allow(
1102 clippy::option_option,
1103 reason = "serde distinguishes a missing run id from an explicitly absent run id"
1104 )]
1105 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1106 current_run_id: Option<Option<RunId>>,
1107 #[allow(
1108 clippy::option_option,
1109 reason = "serde distinguishes a missing pre-run phase from an explicitly absent phase"
1110 )]
1111 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1112 pre_run_phase: Option<Option<MachineLifecyclePreRunPhase>>,
1113 supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1114 #[allow(
1115 clippy::option_option,
1116 reason = "serde distinguishes a missing v4 field from explicit null progress"
1117 )]
1118 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1119 unregister_progress: Option<Option<MachineUnregisterProgressSnapshotStoreWire>>,
1120}
1121
1122#[derive(serde::Deserialize)]
1123#[serde(deny_unknown_fields)]
1124struct MachineLifecycleSnapshotStoreWireV3 {
1125 record_version: u16,
1126 runtime_state: RuntimeState,
1127 binding: MachineLifecycleBindingFactsStoreWire,
1128 supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1129 #[allow(
1130 clippy::option_option,
1131 reason = "serde distinguishes a missing v3 field from explicit null progress"
1132 )]
1133 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1134 unregister_progress: Option<Option<MachineUnregisterProgressSnapshotStoreWire>>,
1135}
1136
1137#[derive(serde::Deserialize)]
1138#[serde(deny_unknown_fields)]
1139struct MachineLifecycleSnapshotStoreWireV2 {
1140 record_version: u16,
1141 runtime_state: RuntimeState,
1142 binding: MachineLifecycleBindingFactsStoreWire,
1143 supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1144}
1145
1146#[derive(serde::Serialize, serde::Deserialize)]
1147#[serde(deny_unknown_fields)]
1148struct MachineUnregisterProgressSnapshotStoreWire {
1149 runtime_loop_drain_pending: bool,
1150 comms_drain_exit_pending: bool,
1151 completion_waiter_drain_pending: bool,
1152 runtime_loop_forced_abort: bool,
1153 comms_drain_forced_abort: bool,
1154}
1155
1156impl From<&MachineUnregisterProgressSnapshot> for MachineUnregisterProgressSnapshotStoreWire {
1157 fn from(snapshot: &MachineUnregisterProgressSnapshot) -> Self {
1158 Self {
1159 runtime_loop_drain_pending: snapshot.runtime_loop_drain_pending(),
1160 comms_drain_exit_pending: snapshot.comms_drain_exit_pending(),
1161 completion_waiter_drain_pending: snapshot.completion_waiter_drain_pending(),
1162 runtime_loop_forced_abort: snapshot.runtime_loop_forced_abort(),
1163 comms_drain_forced_abort: snapshot.comms_drain_forced_abort(),
1164 }
1165 }
1166}
1167
1168impl From<MachineUnregisterProgressSnapshotStoreWire> for MachineUnregisterProgressSnapshot {
1169 fn from(snapshot: MachineUnregisterProgressSnapshotStoreWire) -> Self {
1170 Self::new(
1171 snapshot.runtime_loop_drain_pending,
1172 snapshot.comms_drain_exit_pending,
1173 snapshot.completion_waiter_drain_pending,
1174 snapshot.runtime_loop_forced_abort,
1175 snapshot.comms_drain_forced_abort,
1176 )
1177 }
1178}
1179
1180#[derive(serde::Deserialize)]
1184#[serde(deny_unknown_fields)]
1185struct MachineLifecycleSnapshotStoreWireV1 {
1186 record_version: u16,
1187 runtime_state: RuntimeState,
1188 binding: MachineLifecycleBindingFactsStoreWireV1,
1189}
1190
1191#[derive(serde::Deserialize)]
1192struct MachineLifecycleSnapshotStoreVersionProbe {
1193 record_version: u16,
1194}
1195
1196#[derive(Default, serde::Serialize, serde::Deserialize)]
1197#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1198enum SupervisorAuthoritySnapshotStoreWire {
1199 #[default]
1200 UnboundNoReceipt,
1201 Bound {
1202 binding: SupervisorBindingReceiptStoreWire,
1203 },
1204 RevocationPending {
1205 pending: SupervisorRevocationPendingReceiptStoreWire,
1206 },
1207 RotationOperation {
1208 rotation: SupervisorRotationReceiptStoreWire,
1209 },
1210 RevokedReceipt {
1211 receipt: RevokedSupervisorReceiptStoreWire,
1212 },
1213 WithRotationHistory {
1214 current: Box<SupervisorAuthoritySnapshotStoreWire>,
1215 terminal_receipts: Vec<SupervisorRotationReceiptStoreWire>,
1216 },
1217}
1218
1219#[derive(serde::Serialize, serde::Deserialize)]
1220#[serde(deny_unknown_fields)]
1221struct SupervisorBindingReceiptStoreWire {
1222 name: String,
1223 peer_id: String,
1224 address: String,
1225 signing_public_key: String,
1226 epoch: u64,
1227}
1228
1229impl From<&SupervisorBindingReceipt> for SupervisorBindingReceiptStoreWire {
1230 fn from(receipt: &SupervisorBindingReceipt) -> Self {
1231 Self {
1232 name: receipt.name().to_owned(),
1233 peer_id: receipt.peer_id().to_owned(),
1234 address: receipt.address().to_owned(),
1235 signing_public_key: receipt.signing_public_key().to_owned(),
1236 epoch: receipt.epoch(),
1237 }
1238 }
1239}
1240
1241impl From<SupervisorBindingReceiptStoreWire> for SupervisorBindingReceipt {
1242 fn from(receipt: SupervisorBindingReceiptStoreWire) -> Self {
1243 Self::new(
1244 receipt.name,
1245 receipt.peer_id,
1246 receipt.address,
1247 receipt.signing_public_key,
1248 receipt.epoch,
1249 )
1250 }
1251}
1252
1253#[derive(serde::Serialize, serde::Deserialize)]
1254#[serde(deny_unknown_fields)]
1255struct RevokedSupervisorReceiptStoreWire {
1256 peer_id: String,
1257 signing_public_key: String,
1258 epoch: u64,
1259}
1260
1261#[derive(serde::Serialize, serde::Deserialize)]
1262#[serde(deny_unknown_fields)]
1263struct SupervisorRevocationPendingReceiptStoreWire {
1264 name: String,
1265 peer_id: String,
1266 address: String,
1267 signing_public_key: String,
1268 epoch: u64,
1269}
1270
1271#[derive(serde::Serialize, serde::Deserialize)]
1272#[serde(deny_unknown_fields)]
1273struct SupervisorRotationReceiptStoreWire {
1274 operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1275 phase: SupervisorRotationPersistencePhase,
1276 #[allow(
1277 clippy::option_option,
1278 reason = "serde distinguishes missing from explicit null"
1279 )]
1280 #[serde(default, deserialize_with = "deserialize_present_nullable")]
1281 rejection: Option<Option<SupervisorRotationRejection>>,
1282 previous: SupervisorBindingReceiptStoreWire,
1283 next: SupervisorBindingReceiptStoreWire,
1284}
1285
1286impl From<&SupervisorRotationReceipt> for SupervisorRotationReceiptStoreWire {
1287 fn from(receipt: &SupervisorRotationReceipt) -> Self {
1288 Self {
1289 operation_id: receipt.operation_id(),
1290 phase: receipt.phase(),
1291 rejection: Some(receipt.rejection()),
1292 previous: receipt.previous().into(),
1293 next: receipt.next().into(),
1294 }
1295 }
1296}
1297
1298impl TryFrom<SupervisorRotationReceiptStoreWire> for SupervisorRotationReceipt {
1299 type Error = RuntimeStoreError;
1300
1301 fn try_from(receipt: SupervisorRotationReceiptStoreWire) -> Result<Self, Self::Error> {
1302 Ok(Self::new(
1303 receipt.operation_id,
1304 receipt.phase,
1305 require_present_nullable(receipt.rejection, "supervisor_authority.rotation.rejection")?,
1306 receipt.previous.into(),
1307 receipt.next.into(),
1308 ))
1309 }
1310}
1311
1312impl From<&SupervisorRevocationPendingReceipt> for SupervisorRevocationPendingReceiptStoreWire {
1313 fn from(receipt: &SupervisorRevocationPendingReceipt) -> Self {
1314 Self {
1315 name: receipt.name().to_owned(),
1316 peer_id: receipt.peer_id().to_owned(),
1317 address: receipt.address().to_owned(),
1318 signing_public_key: receipt.signing_public_key().to_owned(),
1319 epoch: receipt.epoch(),
1320 }
1321 }
1322}
1323
1324impl From<SupervisorRevocationPendingReceiptStoreWire> for SupervisorRevocationPendingReceipt {
1325 fn from(receipt: SupervisorRevocationPendingReceiptStoreWire) -> Self {
1326 Self::new(
1327 receipt.name,
1328 receipt.peer_id,
1329 receipt.address,
1330 receipt.signing_public_key,
1331 receipt.epoch,
1332 )
1333 }
1334}
1335
1336impl From<&RevokedSupervisorReceipt> for RevokedSupervisorReceiptStoreWire {
1337 fn from(receipt: &RevokedSupervisorReceipt) -> Self {
1338 Self {
1339 peer_id: receipt.peer_id().to_owned(),
1340 signing_public_key: receipt.signing_public_key().to_owned(),
1341 epoch: receipt.epoch(),
1342 }
1343 }
1344}
1345
1346impl From<RevokedSupervisorReceiptStoreWire> for RevokedSupervisorReceipt {
1347 fn from(receipt: RevokedSupervisorReceiptStoreWire) -> Self {
1348 Self::new(receipt.peer_id, receipt.signing_public_key, receipt.epoch)
1349 }
1350}
1351
1352impl From<&SupervisorAuthoritySnapshot> for SupervisorAuthoritySnapshotStoreWire {
1353 fn from(snapshot: &SupervisorAuthoritySnapshot) -> Self {
1354 match snapshot {
1355 SupervisorAuthoritySnapshot::UnboundNoReceipt => Self::UnboundNoReceipt,
1356 SupervisorAuthoritySnapshot::Bound(binding) => Self::Bound {
1357 binding: binding.into(),
1358 },
1359 SupervisorAuthoritySnapshot::RevocationPending(pending) => Self::RevocationPending {
1360 pending: pending.into(),
1361 },
1362 SupervisorAuthoritySnapshot::RotationOperation(rotation) => Self::RotationOperation {
1363 rotation: rotation.into(),
1364 },
1365 SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Self::RevokedReceipt {
1366 receipt: receipt.into(),
1367 },
1368 SupervisorAuthoritySnapshot::WithRotationHistory {
1369 current,
1370 terminal_receipts,
1371 } => Self::WithRotationHistory {
1372 current: Box::new(current.as_ref().into()),
1373 terminal_receipts: terminal_receipts.values().map(Into::into).collect(),
1374 },
1375 }
1376 }
1377}
1378
1379fn supervisor_authority_read_error(
1380 context: &str,
1381 detail: impl std::fmt::Display,
1382) -> RuntimeStoreError {
1383 RuntimeStoreError::ReadFailed(format!("{context}: {detail}"))
1384}
1385
1386fn validate_supervisor_descriptor(
1387 name: &str,
1388 peer_id: &str,
1389 address: &str,
1390 signing_public_key: &str,
1391 context: &str,
1392) -> Result<(), RuntimeStoreError> {
1393 let pubkey = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
1394 .map_err(|error| supervisor_authority_read_error(context, error))?;
1395 let spec = meerkat_contracts::wire::supervisor_bridge::BridgePeerSpec {
1396 name: name.to_owned(),
1397 peer_id: peer_id.to_owned(),
1398 address: address.to_owned(),
1399 pubkey,
1400 };
1401 meerkat_core::comms::TrustedPeerDescriptor::try_from(&spec)
1402 .map(|_| ())
1403 .map_err(|error| supervisor_authority_read_error(context, error))
1404}
1405
1406fn validate_supervisor_binding_receipt(
1407 receipt: &SupervisorBindingReceipt,
1408 context: &str,
1409) -> Result<(), RuntimeStoreError> {
1410 validate_supervisor_descriptor(
1411 receipt.name(),
1412 receipt.peer_id(),
1413 receipt.address(),
1414 receipt.signing_public_key(),
1415 context,
1416 )
1417}
1418
1419fn validate_revoked_supervisor_receipt(
1420 receipt: &RevokedSupervisorReceipt,
1421 context: &str,
1422) -> Result<(), RuntimeStoreError> {
1423 let pubkey =
1424 crate::comms_drain::decode_supervisor_signing_public_key(receipt.signing_public_key())
1425 .map_err(|error| supervisor_authority_read_error(context, error))?;
1426 if pubkey.iter().all(|byte| *byte == 0) {
1427 return Err(supervisor_authority_read_error(
1428 context,
1429 "supervisor signing public key must be non-zero",
1430 ));
1431 }
1432 let peer_id = meerkat_core::comms::PeerId::parse(receipt.peer_id())
1433 .map_err(|error| supervisor_authority_read_error(context, error))?;
1434 let derived = meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey);
1435 if peer_id != derived {
1436 return Err(supervisor_authority_read_error(
1437 context,
1438 format!("peer id {peer_id} does not match signing-key-derived id {derived}"),
1439 ));
1440 }
1441 Ok(())
1442}
1443
1444fn validate_supervisor_rotation_receipt(
1445 receipt: &SupervisorRotationReceipt,
1446 terminal_history: bool,
1447) -> Result<(), RuntimeStoreError> {
1448 let operation_id = receipt.operation_id();
1449 if operation_id.as_uuid().is_nil() {
1450 return Err(supervisor_authority_read_error(
1451 "supervisor rotation operation",
1452 "operation id must not be the nil UUID",
1453 ));
1454 }
1455 validate_supervisor_binding_receipt(
1456 receipt.previous(),
1457 &format!("supervisor rotation {operation_id} previous authority is invalid"),
1458 )?;
1459
1460 let rejection_matches = matches!(
1461 (receipt.phase(), receipt.rejection()),
1462 (
1463 SupervisorRotationPersistencePhase::PreviousRevokePending
1464 | SupervisorRotationPersistencePhase::NextPublishPending
1465 | SupervisorRotationPersistencePhase::Completed,
1466 None
1467 ) | (SupervisorRotationPersistencePhase::Rejected, Some(_))
1468 );
1469 if !rejection_matches {
1470 return Err(supervisor_authority_read_error(
1471 "supervisor rotation operation",
1472 format!("{operation_id} has inconsistent rejection state"),
1473 ));
1474 }
1475 if terminal_history
1476 && !matches!(
1477 receipt.phase(),
1478 SupervisorRotationPersistencePhase::Completed
1479 | SupervisorRotationPersistencePhase::Rejected
1480 )
1481 {
1482 return Err(supervisor_authority_read_error(
1483 "supervisor rotation history",
1484 format!("{operation_id} is not terminal"),
1485 ));
1486 }
1487
1488 match receipt.phase() {
1489 SupervisorRotationPersistencePhase::PreviousRevokePending
1490 | SupervisorRotationPersistencePhase::NextPublishPending => {
1491 validate_supervisor_binding_receipt(
1492 receipt.next(),
1493 &format!("supervisor rotation {operation_id} target is invalid"),
1494 )?;
1495 if receipt.next().epoch() <= receipt.previous().epoch() {
1496 return Err(supervisor_authority_read_error(
1497 "supervisor rotation operation",
1498 format!(
1499 "{operation_id} target epoch {} does not advance previous epoch {}",
1500 receipt.next().epoch(),
1501 receipt.previous().epoch()
1502 ),
1503 ));
1504 }
1505 }
1506 SupervisorRotationPersistencePhase::Completed => {
1507 validate_supervisor_binding_receipt(
1508 receipt.next(),
1509 &format!("supervisor rotation {operation_id} target is invalid"),
1510 )?;
1511 let exact_current_adoption = receipt.previous() == receipt.next();
1515 if !exact_current_adoption && receipt.next().epoch() <= receipt.previous().epoch() {
1516 return Err(supervisor_authority_read_error(
1517 "supervisor rotation operation",
1518 format!(
1519 "{operation_id} completed target epoch {} does not advance previous epoch {}",
1520 receipt.next().epoch(),
1521 receipt.previous().epoch()
1522 ),
1523 ));
1524 }
1525 }
1526 SupervisorRotationPersistencePhase::Rejected => {
1527 let Some(rejection) = receipt.rejection() else {
1528 return Err(supervisor_authority_read_error(
1529 "supervisor rotation operation",
1530 format!("{operation_id} rejected without a rejection class"),
1531 ));
1532 };
1533 match rejection {
1534 SupervisorRotationRejection::InvalidTarget
1535 | SupervisorRotationRejection::UnsupportedProtocolVersion => {
1536 }
1540 SupervisorRotationRejection::TargetEpochNotAdvanced => {
1541 validate_supervisor_binding_receipt(
1542 receipt.next(),
1543 &format!("supervisor rotation {operation_id} rejected target is invalid"),
1544 )?;
1545 if receipt.next().epoch() > receipt.previous().epoch() {
1546 return Err(supervisor_authority_read_error(
1547 "supervisor rotation operation",
1548 format!(
1549 "{operation_id} rejected as non-advancing but target epoch {} advances previous epoch {}",
1550 receipt.next().epoch(),
1551 receipt.previous().epoch()
1552 ),
1553 ));
1554 }
1555 }
1556 SupervisorRotationRejection::OperationConflict
1557 | SupervisorRotationRejection::NotBound
1558 | SupervisorRotationRejection::SenderMismatch => {
1559 return Err(supervisor_authority_read_error(
1560 "supervisor rotation operation",
1561 format!(
1562 "{operation_id} transient rejection {rejection:?} must not be persisted as a durable receipt"
1563 ),
1564 ));
1565 }
1566 }
1567 }
1568 }
1569 Ok(())
1570}
1571
1572type SupervisorEpochKeyIndex = std::collections::BTreeMap<u64, [u8; 32]>;
1573
1574fn record_supervisor_epoch_key(
1575 epochs: &mut SupervisorEpochKeyIndex,
1576 epoch: u64,
1577 signing_public_key: &str,
1578 context: &str,
1579) -> Result<(), RuntimeStoreError> {
1580 let key = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
1581 .map_err(|error| supervisor_authority_read_error(context, error))?;
1582 if let Some(existing) = epochs.get(&epoch) {
1583 if existing != &key {
1584 return Err(supervisor_authority_read_error(
1585 context,
1586 format!("epoch {epoch} is bound to conflicting supervisor signing keys"),
1587 ));
1588 }
1589 } else {
1590 epochs.insert(epoch, key);
1591 }
1592 Ok(())
1593}
1594
1595fn record_supervisor_binding_epoch(
1596 epochs: &mut SupervisorEpochKeyIndex,
1597 receipt: &SupervisorBindingReceipt,
1598 context: &str,
1599) -> Result<(), RuntimeStoreError> {
1600 record_supervisor_epoch_key(
1601 epochs,
1602 receipt.epoch(),
1603 receipt.signing_public_key(),
1604 context,
1605 )
1606}
1607
1608fn record_rotation_authoritative_epochs(
1609 epochs: &mut SupervisorEpochKeyIndex,
1610 receipt: &SupervisorRotationReceipt,
1611 context: &str,
1612) -> Result<(), RuntimeStoreError> {
1613 record_supervisor_binding_epoch(epochs, receipt.previous(), context)?;
1614 if matches!(
1615 receipt.phase(),
1616 SupervisorRotationPersistencePhase::PreviousRevokePending
1617 | SupervisorRotationPersistencePhase::NextPublishPending
1618 | SupervisorRotationPersistencePhase::Completed
1619 ) {
1620 record_supervisor_binding_epoch(epochs, receipt.next(), context)?;
1621 }
1622 Ok(())
1623}
1624
1625fn record_current_authoritative_epochs(
1626 epochs: &mut SupervisorEpochKeyIndex,
1627 current: &SupervisorAuthoritySnapshot,
1628) -> Result<(), RuntimeStoreError> {
1629 match current {
1630 SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1631 SupervisorAuthoritySnapshot::Bound(binding) => {
1632 record_supervisor_binding_epoch(epochs, binding, "current supervisor authority")
1633 }
1634 SupervisorAuthoritySnapshot::RevocationPending(pending) => record_supervisor_epoch_key(
1635 epochs,
1636 pending.epoch(),
1637 pending.signing_public_key(),
1638 "current pending supervisor revocation authority",
1639 ),
1640 SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1641 record_rotation_authoritative_epochs(
1642 epochs,
1643 rotation,
1644 "current supervisor rotation authority",
1645 )
1646 }
1647 SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => record_supervisor_epoch_key(
1648 epochs,
1649 receipt.epoch(),
1650 receipt.signing_public_key(),
1651 "current revoked supervisor authority",
1652 ),
1653 SupervisorAuthoritySnapshot::WithRotationHistory { .. } => {
1654 Err(RuntimeStoreError::ReadFailed(
1655 "nested supervisor rotation history is not canonical".to_string(),
1656 ))
1657 }
1658 }
1659}
1660
1661fn current_supervisor_epoch(current: &SupervisorAuthoritySnapshot) -> Option<u64> {
1662 match current {
1663 SupervisorAuthoritySnapshot::UnboundNoReceipt => None,
1664 SupervisorAuthoritySnapshot::Bound(binding) => Some(binding.epoch()),
1665 SupervisorAuthoritySnapshot::RevocationPending(pending) => Some(pending.epoch()),
1666 SupervisorAuthoritySnapshot::RotationOperation(rotation) => Some(match rotation.phase() {
1667 SupervisorRotationPersistencePhase::PreviousRevokePending
1668 | SupervisorRotationPersistencePhase::Rejected => rotation.previous().epoch(),
1669 SupervisorRotationPersistencePhase::NextPublishPending
1670 | SupervisorRotationPersistencePhase::Completed => rotation.next().epoch(),
1671 }),
1672 SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Some(receipt.epoch()),
1673 SupervisorAuthoritySnapshot::WithRotationHistory { .. } => None,
1674 }
1675}
1676
1677fn terminal_rotation_authority_epoch(receipt: &SupervisorRotationReceipt) -> u64 {
1678 match receipt.phase() {
1679 SupervisorRotationPersistencePhase::Completed => receipt.next().epoch(),
1680 SupervisorRotationPersistencePhase::Rejected => receipt.previous().epoch(),
1681 SupervisorRotationPersistencePhase::PreviousRevokePending
1682 | SupervisorRotationPersistencePhase::NextPublishPending => receipt.previous().epoch(),
1683 }
1684}
1685
1686fn validate_supervisor_rotation_history_coherence(
1687 current: &SupervisorAuthoritySnapshot,
1688 terminal_receipts: &std::collections::BTreeMap<
1689 meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1690 SupervisorRotationReceipt,
1691 >,
1692) -> Result<(), RuntimeStoreError> {
1693 let Some(current_epoch) = current_supervisor_epoch(current) else {
1694 return Err(RuntimeStoreError::ReadFailed(
1695 "supervisor rotation history requires a current authority epoch".to_string(),
1696 ));
1697 };
1698
1699 let mut epochs = SupervisorEpochKeyIndex::new();
1700 record_current_authoritative_epochs(&mut epochs, current)?;
1701 let mut history_high_water = 0;
1702 for receipt in terminal_receipts.values() {
1703 record_rotation_authoritative_epochs(
1704 &mut epochs,
1705 receipt,
1706 "supervisor rotation history authority",
1707 )?;
1708 history_high_water = history_high_water.max(terminal_rotation_authority_epoch(receipt));
1709 }
1710 if current_epoch < history_high_water {
1711 return Err(RuntimeStoreError::ReadFailed(format!(
1712 "current supervisor epoch {current_epoch} is below terminal rotation history high-water {history_high_water}"
1713 )));
1714 }
1715 Ok(())
1716}
1717
1718fn validate_supervisor_authority_snapshot(
1719 snapshot: &SupervisorAuthoritySnapshot,
1720) -> Result<(), RuntimeStoreError> {
1721 match snapshot {
1722 SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1723 SupervisorAuthoritySnapshot::Bound(binding) => {
1724 validate_supervisor_binding_receipt(binding, "bound supervisor is invalid")
1725 }
1726 SupervisorAuthoritySnapshot::RevocationPending(pending) => validate_supervisor_descriptor(
1727 pending.name(),
1728 pending.peer_id(),
1729 pending.address(),
1730 pending.signing_public_key(),
1731 "pending supervisor revocation authority is invalid",
1732 ),
1733 SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1734 validate_supervisor_rotation_receipt(rotation, false)
1735 }
1736 SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => {
1737 validate_revoked_supervisor_receipt(receipt, "revoked supervisor receipt is invalid")
1738 }
1739 SupervisorAuthoritySnapshot::WithRotationHistory {
1740 current,
1741 terminal_receipts,
1742 } => {
1743 if matches!(
1744 current.as_ref(),
1745 SupervisorAuthoritySnapshot::WithRotationHistory { .. }
1746 ) {
1747 return Err(RuntimeStoreError::ReadFailed(
1748 "nested supervisor rotation history is not canonical".to_string(),
1749 ));
1750 }
1751 if terminal_receipts.is_empty() {
1752 return Err(RuntimeStoreError::ReadFailed(
1753 "empty supervisor rotation history wrapper is not canonical".to_string(),
1754 ));
1755 }
1756 validate_supervisor_authority_snapshot(current)?;
1757 for (operation_id, receipt) in terminal_receipts {
1758 if operation_id != &receipt.operation_id() {
1759 return Err(RuntimeStoreError::ReadFailed(format!(
1760 "supervisor rotation history key {operation_id} does not match receipt id {}",
1761 receipt.operation_id()
1762 )));
1763 }
1764 validate_supervisor_rotation_receipt(receipt, true)?;
1765 }
1766 if let SupervisorAuthoritySnapshot::RotationOperation(active) = current.as_ref()
1767 && terminal_receipts.contains_key(&active.operation_id())
1768 {
1769 return Err(RuntimeStoreError::ReadFailed(
1770 "active supervisor rotation is duplicated in terminal history".to_string(),
1771 ));
1772 }
1773 validate_supervisor_rotation_history_coherence(current, terminal_receipts)
1774 }
1775 }
1776}
1777
1778impl TryFrom<SupervisorAuthoritySnapshotStoreWire> for SupervisorAuthoritySnapshot {
1779 type Error = RuntimeStoreError;
1780
1781 fn try_from(snapshot: SupervisorAuthoritySnapshotStoreWire) -> Result<Self, Self::Error> {
1782 match snapshot {
1783 SupervisorAuthoritySnapshotStoreWire::UnboundNoReceipt => Ok(Self::UnboundNoReceipt),
1784 SupervisorAuthoritySnapshotStoreWire::Bound { binding } => {
1785 let binding = binding.into();
1786 validate_supervisor_binding_receipt(&binding, "bound supervisor is invalid")?;
1787 Ok(Self::Bound(binding))
1788 }
1789 SupervisorAuthoritySnapshotStoreWire::RevocationPending { pending } => {
1790 let pending: SupervisorRevocationPendingReceipt = pending.into();
1791 validate_supervisor_descriptor(
1792 pending.name(),
1793 pending.peer_id(),
1794 pending.address(),
1795 pending.signing_public_key(),
1796 "pending supervisor revocation authority is invalid",
1797 )?;
1798 Ok(Self::RevocationPending(pending))
1799 }
1800 SupervisorAuthoritySnapshotStoreWire::RotationOperation { rotation } => {
1801 let receipt: SupervisorRotationReceipt = rotation.try_into()?;
1802 validate_supervisor_rotation_receipt(&receipt, false)?;
1803 Ok(Self::RotationOperation(receipt))
1804 }
1805 SupervisorAuthoritySnapshotStoreWire::RevokedReceipt { receipt } => {
1806 let receipt = receipt.into();
1807 validate_revoked_supervisor_receipt(
1808 &receipt,
1809 "revoked supervisor receipt is invalid",
1810 )?;
1811 Ok(Self::RevokedReceipt(receipt))
1812 }
1813 SupervisorAuthoritySnapshotStoreWire::WithRotationHistory {
1814 current,
1815 terminal_receipts,
1816 } => {
1817 if terminal_receipts.is_empty() {
1818 return Err(RuntimeStoreError::ReadFailed(
1819 "empty supervisor rotation history wrapper is not canonical".to_string(),
1820 ));
1821 }
1822 let current = Self::try_from(*current)?;
1823 if matches!(current, Self::WithRotationHistory { .. }) {
1824 return Err(RuntimeStoreError::ReadFailed(
1825 "nested supervisor rotation history is not canonical".to_string(),
1826 ));
1827 }
1828 let mut receipts = std::collections::BTreeMap::new();
1829 for wire in terminal_receipts {
1830 let receipt: SupervisorRotationReceipt = wire.try_into()?;
1831 validate_supervisor_rotation_receipt(&receipt, true)?;
1832 if receipts.insert(receipt.operation_id(), receipt).is_some() {
1833 return Err(RuntimeStoreError::ReadFailed(
1834 "supervisor rotation history contains a duplicate operation id"
1835 .to_string(),
1836 ));
1837 }
1838 }
1839 if let Self::RotationOperation(active) = ¤t
1840 && receipts.contains_key(&active.operation_id())
1841 {
1842 return Err(RuntimeStoreError::ReadFailed(
1843 "active supervisor rotation is duplicated in terminal history".to_string(),
1844 ));
1845 }
1846 let snapshot = Self::WithRotationHistory {
1847 current: Box::new(current),
1848 terminal_receipts: receipts,
1849 };
1850 validate_supervisor_authority_snapshot(&snapshot)?;
1851 Ok(snapshot)
1852 }
1853 }
1854 }
1855}
1856
1857impl From<&MachineLifecycleSnapshot> for MachineLifecycleSnapshotStoreWire {
1858 fn from(snapshot: &MachineLifecycleSnapshot) -> Self {
1859 Self {
1860 record_version: MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
1861 runtime_state: snapshot.runtime_state(),
1862 binding: snapshot.binding().into(),
1863 current_run_id: snapshot.run().current_run_id().cloned(),
1864 pre_run_phase: snapshot.run().pre_run_phase(),
1865 supervisor_authority: snapshot.supervisor_authority().into(),
1866 unregister_progress: snapshot.unregister_progress().map(Into::into),
1867 }
1868 }
1869}
1870
1871fn validate_unregister_progress_snapshot(
1872 progress: Option<&MachineUnregisterProgressSnapshot>,
1873) -> Result<(), RuntimeStoreError> {
1874 if let Some(progress) = progress {
1875 if progress.runtime_loop_drain_pending() && progress.runtime_loop_forced_abort() {
1876 return Err(RuntimeStoreError::ReadFailed(
1877 "unregister runtime-loop forced disposition cannot precede obligation closure"
1878 .into(),
1879 ));
1880 }
1881 if progress.comms_drain_exit_pending() && progress.comms_drain_forced_abort() {
1882 return Err(RuntimeStoreError::ReadFailed(
1883 "unregister comms-drain forced disposition cannot precede obligation closure"
1884 .into(),
1885 ));
1886 }
1887 }
1888 Ok(())
1889}
1890
1891impl TryFrom<MachineLifecycleSnapshotStoreWireV3> for MachineLifecycleSnapshot {
1892 type Error = RuntimeStoreError;
1893
1894 fn try_from(record: MachineLifecycleSnapshotStoreWireV3) -> Result<Self, Self::Error> {
1895 if record.record_version != UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1896 return Err(RuntimeStoreError::ReadFailed(format!(
1897 "unsupported machine lifecycle store record version {}",
1898 record.record_version
1899 )));
1900 }
1901 let unregister_progress =
1902 require_present_nullable(record.unregister_progress, "unregister_progress")?
1903 .map(Into::into);
1904 validate_unregister_progress_snapshot(unregister_progress.as_ref())?;
1905 Ok(Self::new_with_unregister_progress(
1906 record.runtime_state,
1907 record.binding.try_into()?,
1908 record.supervisor_authority.try_into()?,
1909 unregister_progress,
1910 ))
1911 }
1912}
1913
1914fn decode_machine_lifecycle_observation_v4(
1915 bytes: &[u8],
1916) -> Result<DecodedMachineLifecycleObservation, RuntimeStoreError> {
1917 let record = serde_json::from_slice::<MachineLifecycleObservationStoreWireV4>(bytes)
1918 .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1919 if record.record_version != MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1920 return Err(RuntimeStoreError::ReadFailed(format!(
1921 "unsupported machine lifecycle store record version {}",
1922 record.record_version
1923 )));
1924 }
1925 let runtime_state = require_present_nullable(record.runtime_state, "runtime_state")?;
1926 let current_run_id = require_present_nullable(record.current_run_id, "current_run_id")?;
1927 let pre_run_phase = require_present_nullable(record.pre_run_phase, "pre_run_phase")?;
1928 let unregister_progress =
1929 require_present_nullable(record.unregister_progress, "unregister_progress")?
1930 .map(Into::into);
1931 validate_unregister_progress_snapshot(unregister_progress.as_ref())?;
1932 Ok(DecodedMachineLifecycleObservation {
1933 record_version: record.record_version,
1934 runtime_state,
1935 binding: record.binding.try_into()?,
1936 run: MachineLifecycleRunFacts::new(current_run_id, pre_run_phase),
1937 supervisor_authority: record.supervisor_authority.try_into()?,
1938 unregister_progress,
1939 })
1940}
1941
1942fn decoded_machine_lifecycle_from_snapshot(
1943 record_version: u16,
1944 snapshot: MachineLifecycleSnapshot,
1945) -> DecodedMachineLifecycleObservation {
1946 DecodedMachineLifecycleObservation {
1947 record_version,
1948 runtime_state: Some(snapshot.runtime_state),
1949 binding: snapshot.binding,
1950 run: snapshot.run,
1951 supervisor_authority: snapshot.supervisor_authority,
1952 unregister_progress: snapshot.unregister_progress,
1953 }
1954}
1955
1956fn decode_machine_lifecycle_store_record(
1957 bytes: &[u8],
1958) -> Result<MachineLifecycleSnapshot, RuntimeStoreError> {
1959 let version = serde_json::from_slice::<MachineLifecycleSnapshotStoreVersionProbe>(bytes)
1960 .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1961 match version.record_version {
1962 LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1963 let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV1>(bytes)
1964 .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1965 if record.record_version != LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1966 return Err(RuntimeStoreError::ReadFailed(format!(
1967 "unsupported machine lifecycle store record version {}",
1968 record.record_version
1969 )));
1970 }
1971 Ok(MachineLifecycleSnapshot::new(
1972 record.runtime_state,
1973 record.binding.into(),
1974 SupervisorAuthoritySnapshot::UnboundNoReceipt,
1975 ))
1976 }
1977 SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1978 let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV2>(bytes)
1979 .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1980 if record.record_version != SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1981 return Err(RuntimeStoreError::ReadFailed(format!(
1982 "unsupported machine lifecycle store record version {}",
1983 record.record_version
1984 )));
1985 }
1986 Ok(MachineLifecycleSnapshot::new(
1987 record.runtime_state,
1988 record.binding.try_into()?,
1989 record.supervisor_authority.try_into()?,
1990 ))
1991 }
1992 UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1993 let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV3>(bytes)
1994 .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1995 MachineLifecycleSnapshot::try_from(record)
1996 }
1997 MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1998 let record = decode_machine_lifecycle_observation_v4(bytes)?;
1999 let runtime_state = record.runtime_state.ok_or_else(|| {
2000 RuntimeStoreError::ReadFailed(
2001 "machine lifecycle runtime_state cannot be null for strict recovery".into(),
2002 )
2003 })?;
2004 Ok(
2005 MachineLifecycleSnapshot::new_with_run_and_unregister_progress(
2006 runtime_state,
2007 record.binding,
2008 record.run,
2009 record.supervisor_authority,
2010 record.unregister_progress,
2011 ),
2012 )
2013 }
2014 unsupported => Err(RuntimeStoreError::ReadFailed(format!(
2015 "unsupported machine lifecycle store record version {unsupported}"
2016 ))),
2017 }
2018}
2019
2020#[derive(serde::Deserialize)]
2021struct MachineLifecycleRawVersionProbe {
2022 record_version: u64,
2023}
2024
2025fn machine_lifecycle_record_version(bytes: &[u8]) -> Result<u64, String> {
2026 serde_json::from_slice::<MachineLifecycleRawVersionProbe>(bytes)
2027 .map(|probe| probe.record_version)
2028 .map_err(|error| {
2029 format!("machine lifecycle record_version is not uniquely readable: {error}")
2030 })
2031}
2032
2033fn classify_machine_lifecycle_record(bytes: &[u8]) -> MachineLifecycleObservation {
2034 let version = MachineLifecycleObservationVersion::from_raw_record(bytes);
2035 let evidence_digest = version.as_str().to_owned();
2036 let record_version = match machine_lifecycle_record_version(bytes) {
2037 Ok(record_version) => record_version,
2038 Err(detail) => {
2039 return MachineLifecycleObservation::Malformed {
2040 record_version: None,
2041 evidence_digest,
2042 version,
2043 detail,
2044 };
2045 }
2046 };
2047
2048 let supported = [
2049 u64::from(LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2050 u64::from(SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2051 u64::from(UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2052 u64::from(MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2053 ];
2054 if !supported.contains(&record_version) {
2055 return MachineLifecycleObservation::Unsupported {
2056 record_version,
2057 evidence_digest,
2058 version,
2059 };
2060 }
2061
2062 let decoded = if record_version == u64::from(MACHINE_LIFECYCLE_STORE_RECORD_VERSION) {
2063 decode_machine_lifecycle_observation_v4(bytes)
2064 } else {
2065 decode_machine_lifecycle_store_record(bytes).map(|snapshot| {
2066 decoded_machine_lifecycle_from_snapshot(record_version as u16, snapshot)
2067 })
2068 };
2069 match decoded {
2070 Ok(record) => MachineLifecycleObservation::Decoded { record, version },
2071 Err(error) => MachineLifecycleObservation::Malformed {
2072 record_version: Some(record_version),
2073 evidence_digest,
2074 version,
2075 detail: error.to_string(),
2076 },
2077 }
2078}
2079
2080fn replacement_repair_blocked(
2081 evidence_digest: Option<String>,
2082 detail: impl Into<String>,
2083) -> RuntimeStoreError {
2084 RuntimeStoreError::MachineLifecycleRepairBlocked {
2085 evidence_digest,
2086 detail: detail.into(),
2087 }
2088}
2089
2090fn validate_machine_lifecycle_replacement(
2098 current: &MachineLifecycleObservation,
2099 _current_raw: Option<&[u8]>,
2100 _replacement: &MachineLifecycleSnapshot,
2101) -> Result<(), RuntimeStoreError> {
2102 match current {
2103 MachineLifecycleObservation::Missing | MachineLifecycleObservation::Decoded { .. } => {
2104 Ok(())
2105 }
2106 MachineLifecycleObservation::Unsupported {
2107 evidence_digest,
2108 record_version,
2109 ..
2110 } => Err(replacement_repair_blocked(
2111 Some(evidence_digest.clone()),
2112 format!(
2113 "unsupported lifecycle record version {record_version} cannot prove fencing semantics"
2114 ),
2115 )),
2116 MachineLifecycleObservation::Malformed {
2117 evidence_digest,
2118 detail,
2119 ..
2120 } => Err(replacement_repair_blocked(
2121 Some(evidence_digest.clone()),
2122 format!("malformed lifecycle evidence is not reclaimable: {detail}"),
2123 )),
2124 }
2125}
2126
2127struct PreparedMachineLifecycleReplacement {
2128 snapshot: MachineLifecycleSnapshot,
2129 bytes: Vec<u8>,
2130 version: MachineLifecycleObservationVersion,
2131}
2132
2133impl PreparedMachineLifecycleReplacement {
2134 fn preserve_observed_custody(
2138 mut self,
2139 current: &MachineLifecycleObservation,
2140 ) -> Result<Self, RuntimeStoreError> {
2141 if let MachineLifecycleObservation::Decoded { record, .. } = current {
2142 self.snapshot.supervisor_authority = record.supervisor_authority().clone();
2143 self.snapshot.unregister_progress = record.unregister_progress().cloned();
2144 self.bytes = MachineLifecycleStoreRecord::from_snapshot(&self.snapshot).encode()?;
2145 self.version = MachineLifecycleObservationVersion::from_raw_record(&self.bytes);
2146 }
2147 Ok(self)
2148 }
2149}
2150
2151fn prepare_machine_lifecycle_replacement(
2152 commit: MachineLifecycleCommit,
2153) -> Result<PreparedMachineLifecycleReplacement, RuntimeStoreError> {
2154 let bytes = commit.store_record().encode()?;
2155 let version = MachineLifecycleObservationVersion::from_raw_record(&bytes);
2156 Ok(PreparedMachineLifecycleReplacement {
2157 snapshot: commit.into_snapshot(),
2158 bytes,
2159 version,
2160 })
2161}
2162
2163fn decoded_prepared_machine_lifecycle_replacement(
2164 replacement: &PreparedMachineLifecycleReplacement,
2165) -> Result<DecodedMachineLifecycleObservation, RuntimeStoreError> {
2166 match classify_machine_lifecycle_record(&replacement.bytes) {
2167 MachineLifecycleObservation::Decoded { record, .. } => Ok(record),
2168 other => Err(RuntimeStoreError::Internal(format!(
2169 "machine-authorized lifecycle replacement did not decode: {other:?}"
2170 ))),
2171 }
2172}
2173
2174pub async fn load_runtime_state(
2182 store: &dyn RuntimeStore,
2183 runtime_id: &LogicalRuntimeId,
2184) -> Result<Option<RuntimeState>, RuntimeStoreError> {
2185 Ok(load_machine_lifecycle(store, runtime_id)
2186 .await?
2187 .map(|snapshot| snapshot.runtime_state()))
2188}
2189
2190pub(crate) async fn load_machine_lifecycle(
2191 store: &dyn RuntimeStore,
2192 runtime_id: &LogicalRuntimeId,
2193) -> Result<Option<MachineLifecycleSnapshot>, RuntimeStoreError> {
2194 store
2195 .load_machine_lifecycle_record(runtime_id)
2196 .await?
2197 .map(|bytes| decode_machine_lifecycle_store_record(&bytes))
2198 .transpose()
2199}
2200
2201#[derive(Debug, Clone, PartialEq, Eq)]
2207pub struct MachineLifecycleStoreRecord {
2208 snapshot: MachineLifecycleSnapshot,
2209}
2210
2211impl MachineLifecycleStoreRecord {
2212 pub(crate) fn from_snapshot(snapshot: &MachineLifecycleSnapshot) -> Self {
2213 Self {
2214 snapshot: snapshot.clone(),
2215 }
2216 }
2217
2218 pub fn encode(&self) -> Result<Vec<u8>, RuntimeStoreError> {
2219 validate_supervisor_authority_snapshot(self.snapshot.supervisor_authority())
2220 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2221 validate_unregister_progress_snapshot(self.snapshot.unregister_progress())
2222 .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2223 let wire = MachineLifecycleSnapshotStoreWire::from(&self.snapshot);
2224 serde_json::to_vec(&wire).map_err(|err| RuntimeStoreError::WriteFailed(err.to_string()))
2225 }
2226}
2227
2228#[derive(Debug, Clone, PartialEq, Eq)]
2234pub struct MachineLifecycleCommit {
2235 snapshot: MachineLifecycleSnapshot,
2236}
2237
2238impl MachineLifecycleCommit {
2239 #[cfg(test)]
2240 pub(crate) fn new_with_binding(
2241 runtime_state: RuntimeState,
2242 binding: MachineLifecycleBindingFacts,
2243 supervisor_authority: SupervisorAuthoritySnapshot,
2244 ) -> Self {
2245 Self::new_with_binding_and_unregister_progress(
2246 runtime_state,
2247 binding,
2248 supervisor_authority,
2249 None,
2250 )
2251 }
2252
2253 pub(crate) fn new_with_binding_and_unregister_progress(
2254 runtime_state: RuntimeState,
2255 binding: MachineLifecycleBindingFacts,
2256 supervisor_authority: SupervisorAuthoritySnapshot,
2257 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
2258 ) -> Self {
2259 Self::new_with_binding_run_and_unregister_progress(
2260 runtime_state,
2261 binding,
2262 MachineLifecycleRunFacts::default(),
2263 supervisor_authority,
2264 unregister_progress,
2265 )
2266 }
2267
2268 pub(crate) fn new_with_binding_run_and_unregister_progress(
2269 runtime_state: RuntimeState,
2270 binding: MachineLifecycleBindingFacts,
2271 run: MachineLifecycleRunFacts,
2272 supervisor_authority: SupervisorAuthoritySnapshot,
2273 unregister_progress: Option<MachineUnregisterProgressSnapshot>,
2274 ) -> Self {
2275 Self {
2276 snapshot: MachineLifecycleSnapshot::new_with_run_and_unregister_progress(
2277 runtime_state,
2278 binding,
2279 run,
2280 supervisor_authority,
2281 unregister_progress,
2282 ),
2283 }
2284 }
2285
2286 pub fn runtime_state(&self) -> RuntimeState {
2288 self.snapshot.runtime_state()
2289 }
2290
2291 pub fn snapshot(&self) -> &MachineLifecycleSnapshot {
2293 &self.snapshot
2294 }
2295
2296 pub fn store_record(&self) -> MachineLifecycleStoreRecord {
2298 MachineLifecycleStoreRecord::from_snapshot(&self.snapshot)
2299 }
2300
2301 pub(crate) fn into_snapshot(self) -> MachineLifecycleSnapshot {
2302 self.snapshot
2303 }
2304}
2305
2306#[derive(Debug, Clone)]
2313pub struct UnregisterFinalizationCommit {
2314 machine_lifecycle: MachineLifecycleCommit,
2315 input_states: Vec<InputStatePersistenceRecord>,
2316 retired_ops_epoch: meerkat_core::RuntimeEpochId,
2317}
2318
2319impl UnregisterFinalizationCommit {
2320 pub(crate) fn new(
2321 machine_lifecycle: MachineLifecycleCommit,
2322 input_states: Vec<InputStatePersistenceRecord>,
2323 retired_ops_epoch: meerkat_core::RuntimeEpochId,
2324 _authority: crate::meerkat_machine::DeleteOpsFinalizationAuthority,
2325 ) -> Self {
2326 Self {
2327 machine_lifecycle,
2328 input_states,
2329 retired_ops_epoch,
2330 }
2331 }
2332
2333 pub(crate) fn into_parts(
2334 self,
2335 ) -> (
2336 MachineLifecycleSnapshot,
2337 Vec<InputStatePersistenceRecord>,
2338 meerkat_core::RuntimeEpochId,
2339 ) {
2340 (
2341 self.machine_lifecycle.into_snapshot(),
2342 self.input_states,
2343 self.retired_ops_epoch,
2344 )
2345 }
2346
2347 pub fn lifecycle_store_record(&self) -> MachineLifecycleStoreRecord {
2351 self.machine_lifecycle.store_record()
2352 }
2353
2354 pub fn input_states(&self) -> &[InputStatePersistenceRecord] {
2356 &self.input_states
2357 }
2358
2359 pub fn retired_ops_epoch(&self) -> &meerkat_core::RuntimeEpochId {
2361 &self.retired_ops_epoch
2362 }
2363}
2364
2365#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2376#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2377pub trait RuntimeStore: Send + Sync {
2378 fn supports_compaction_projection_outbox(&self) -> bool {
2382 false
2383 }
2384
2385 fn auth_authority_key(&self) -> Option<String> {
2388 None
2389 }
2390
2391 fn persist_auth_oauth_flow_snapshot(
2397 &self,
2398 snapshot_json: &[u8],
2399 ) -> Result<(), RuntimeStoreError> {
2400 let _ = snapshot_json;
2401 Err(RuntimeStoreError::Unsupported(
2402 "persist_auth_oauth_flow_snapshot".into(),
2403 ))
2404 }
2405
2406 fn load_auth_oauth_flow_snapshot(&self) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2408 Err(RuntimeStoreError::Unsupported(
2409 "load_auth_oauth_flow_snapshot".into(),
2410 ))
2411 }
2412
2413 fn update_auth_oauth_flow_snapshot(
2419 &self,
2420 _update: &mut AuthOAuthFlowSnapshotUpdate<'_>,
2421 ) -> Result<(), RuntimeStoreError> {
2422 Err(RuntimeStoreError::Unsupported(
2423 "update_auth_oauth_flow_snapshot".into(),
2424 ))
2425 }
2426
2427 async fn commit_session_snapshot(
2432 &self,
2433 runtime_id: &LogicalRuntimeId,
2434 session_delta: SessionDelta,
2435 ) -> Result<(), RuntimeStoreError>;
2436
2437 async fn commit_session_transcript_rewrite_snapshot(
2443 &self,
2444 runtime_id: &LogicalRuntimeId,
2445 session_delta: SessionDelta,
2446 commit: &meerkat_core::TranscriptRewriteCommit,
2447 ) -> Result<(), RuntimeStoreError> {
2448 let _ = (runtime_id, session_delta, commit);
2449 Err(RuntimeStoreError::Unsupported(
2450 "commit_session_transcript_rewrite_snapshot".into(),
2451 ))
2452 }
2453
2454 async fn atomic_apply(
2471 &self,
2472 runtime_id: &LogicalRuntimeId,
2473 session_delta: Option<SessionDelta>,
2474 receipt: RunBoundaryReceipt,
2475 input_updates: Vec<InputStatePersistenceRecord>,
2476 session_store_key: Option<meerkat_core::types::SessionId>,
2477 ) -> Result<(), RuntimeStoreError>;
2478
2479 async fn load_pending_compaction_projections(
2482 &self,
2483 runtime_id: &LogicalRuntimeId,
2484 ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
2485 let _ = runtime_id;
2486 Err(RuntimeStoreError::Unsupported(
2487 "load_pending_compaction_projections".to_string(),
2488 ))
2489 }
2490
2491 async fn mark_compaction_projection_finalized(
2498 &self,
2499 runtime_id: &LogicalRuntimeId,
2500 projection: &meerkat_core::CompactionProjectionId,
2501 ) -> Result<(), RuntimeStoreError> {
2502 let _ = (runtime_id, projection);
2503 Err(RuntimeStoreError::Unsupported(
2504 "mark_compaction_projection_finalized".to_string(),
2505 ))
2506 }
2507
2508 async fn atomic_apply_with_machine_lifecycle(
2516 &self,
2517 runtime_id: &LogicalRuntimeId,
2518 session_delta: SessionDelta,
2519 receipt: RunBoundaryReceipt,
2520 machine_lifecycle: MachineLifecycleCommit,
2521 input_updates: Vec<InputStatePersistenceRecord>,
2522 session_store_key: meerkat_core::types::SessionId,
2523 ) -> Result<(), RuntimeStoreError> {
2524 let _ = (
2525 runtime_id,
2526 session_delta,
2527 receipt,
2528 machine_lifecycle,
2529 input_updates,
2530 session_store_key,
2531 );
2532 Err(RuntimeStoreError::Unsupported(
2533 "atomic_apply_with_machine_lifecycle".to_string(),
2534 ))
2535 }
2536
2537 async fn load_input_states(
2539 &self,
2540 runtime_id: &LogicalRuntimeId,
2541 ) -> Result<Vec<StoredInputState>, RuntimeStoreError>;
2542
2543 async fn load_boundary_receipt(
2545 &self,
2546 runtime_id: &LogicalRuntimeId,
2547 run_id: &RunId,
2548 sequence: u64,
2549 ) -> Result<Option<RunBoundaryReceipt>, RuntimeStoreError>;
2550
2551 async fn load_session_snapshot(
2553 &self,
2554 runtime_id: &LogicalRuntimeId,
2555 ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2556
2557 async fn clear_session_snapshot(
2565 &self,
2566 runtime_id: &LogicalRuntimeId,
2567 ) -> Result<(), RuntimeStoreError>;
2568
2569 async fn replace_session_snapshot_if_current(
2576 &self,
2577 runtime_id: &LogicalRuntimeId,
2578 expected_current: &[u8],
2579 replacement: Vec<u8>,
2580 ) -> Result<bool, RuntimeStoreError>;
2581
2582 async fn clear_session_snapshot_if_current(
2587 &self,
2588 runtime_id: &LogicalRuntimeId,
2589 expected_current: &[u8],
2590 ) -> Result<bool, RuntimeStoreError>;
2591
2592 async fn is_runtime_projection_quarantined(
2604 &self,
2605 runtime_id: &LogicalRuntimeId,
2606 ) -> Result<bool, RuntimeStoreError> {
2607 let _ = runtime_id;
2608 Ok(false)
2609 }
2610
2611 async fn persist_input_state(
2613 &self,
2614 runtime_id: &LogicalRuntimeId,
2615 state: &InputStatePersistenceRecord,
2616 ) -> Result<(), RuntimeStoreError>;
2617
2618 async fn persist_input_states_atomically(
2622 &self,
2623 _runtime_id: &LogicalRuntimeId,
2624 states: &[InputStatePersistenceRecord],
2625 ) -> Result<(), RuntimeStoreError> {
2626 if states.is_empty() {
2627 return Ok(());
2628 }
2629 Err(RuntimeStoreError::Unsupported(
2630 "persist_input_states_atomically".to_string(),
2631 ))
2632 }
2633
2634 async fn compare_and_swap_input_states_atomically(
2649 &self,
2650 _runtime_id: &LogicalRuntimeId,
2651 expected: &[StoredInputState],
2652 replacements: &[InputStatePersistenceRecord],
2653 ) -> Result<InputStateBatchCasOutcome, RuntimeStoreError> {
2654 let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2655 if prepared.is_empty() {
2656 return Ok(InputStateBatchCasOutcome::Swapped);
2657 }
2658 Err(RuntimeStoreError::Unsupported(
2659 "compare_and_swap_input_states_atomically".to_string(),
2660 ))
2661 }
2662
2663 async fn compare_and_swap_input_states_atomically_with_fence(
2672 &self,
2673 runtime_id: &LogicalRuntimeId,
2674 expected: &[StoredInputState],
2675 replacements: &[InputStatePersistenceRecord],
2676 write_fence: std::sync::Arc<dyn RuntimeStoreWriteFence>,
2677 ) -> Result<FencedInputStateBatchCasOutcome, RuntimeStoreError> {
2678 let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2679 if prepared.is_empty() {
2680 return Ok(FencedInputStateBatchCasOutcome::Swapped);
2681 }
2682 let _ = (runtime_id, write_fence);
2683 Err(RuntimeStoreError::Unsupported(
2684 "compare_and_swap_input_states_atomically_with_fence".to_string(),
2685 ))
2686 }
2687
2688 async fn load_input_state(
2690 &self,
2691 runtime_id: &LogicalRuntimeId,
2692 input_id: &InputId,
2693 ) -> Result<Option<StoredInputState>, RuntimeStoreError>;
2694
2695 async fn observe_machine_lifecycle(
2702 &self,
2703 runtime_id: &LogicalRuntimeId,
2704 ) -> Result<MachineLifecycleObservation, RuntimeStoreError> {
2705 let _ = runtime_id;
2706 Err(RuntimeStoreError::Unsupported(
2707 "observe_machine_lifecycle".to_string(),
2708 ))
2709 }
2710
2711 async fn compare_and_swap_machine_lifecycle(
2723 &self,
2724 runtime_id: &LogicalRuntimeId,
2725 expected: MachineLifecycleExpectedVersion,
2726 replacement: MachineLifecycleCommit,
2727 ) -> Result<MachineLifecycleCasOutcome, RuntimeStoreError> {
2728 let _ = (runtime_id, expected, replacement);
2729 Err(RuntimeStoreError::Unsupported(
2730 "compare_and_swap_machine_lifecycle".to_string(),
2731 ))
2732 }
2733
2734 async fn compare_and_swap_machine_lifecycle_with_fence(
2743 &self,
2744 runtime_id: &LogicalRuntimeId,
2745 expected: MachineLifecycleExpectedVersion,
2746 replacement: MachineLifecycleCommit,
2747 write_fence: std::sync::Arc<dyn RuntimeStoreWriteFence>,
2748 ) -> Result<FencedMachineLifecycleCasOutcome, RuntimeStoreError> {
2749 let _ = (runtime_id, expected, replacement, write_fence);
2750 Err(RuntimeStoreError::Unsupported(
2751 "compare_and_swap_machine_lifecycle_with_fence".to_string(),
2752 ))
2753 }
2754
2755 async fn load_machine_lifecycle_record(
2763 &self,
2764 runtime_id: &LogicalRuntimeId,
2765 ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2766
2767 async fn commit_machine_lifecycle(
2774 &self,
2775 runtime_id: &LogicalRuntimeId,
2776 commit: MachineLifecycleCommit,
2777 input_states: &[InputStatePersistenceRecord],
2778 ) -> Result<(), RuntimeStoreError>;
2779
2780 async fn commit_unregister_finalization(
2812 &self,
2813 runtime_id: &LogicalRuntimeId,
2814 finalization: UnregisterFinalizationCommit,
2815 ) -> Result<(), RuntimeStoreError> {
2816 let _ = (runtime_id, finalization);
2817 Err(RuntimeStoreError::Unsupported(
2818 "commit_unregister_finalization".into(),
2819 ))
2820 }
2821
2822 async fn initialize_ops_lifecycle_if_absent(
2844 &self,
2845 runtime_id: &LogicalRuntimeId,
2846 candidate: &crate::ops_lifecycle::PersistedOpsSnapshot,
2847 ) -> Result<crate::ops_lifecycle::PersistedOpsSnapshot, RuntimeStoreError> {
2848 let _ = (runtime_id, candidate);
2849 Err(RuntimeStoreError::Unsupported(
2850 "initialize_ops_lifecycle_if_absent".into(),
2851 ))
2852 }
2853
2854 async fn persist_ops_lifecycle(
2856 &self,
2857 runtime_id: &LogicalRuntimeId,
2858 snapshot: &crate::ops_lifecycle::PersistedOpsSnapshot,
2859 ) -> Result<(), RuntimeStoreError> {
2860 let _ = (runtime_id, snapshot);
2861 Err(RuntimeStoreError::Unsupported(
2862 "persist_ops_lifecycle".into(),
2863 ))
2864 }
2865
2866 async fn load_ops_lifecycle(
2868 &self,
2869 runtime_id: &LogicalRuntimeId,
2870 ) -> Result<Option<crate::ops_lifecycle::PersistedOpsSnapshot>, RuntimeStoreError> {
2871 let _ = runtime_id;
2872 Err(RuntimeStoreError::Unsupported("load_ops_lifecycle".into()))
2873 }
2874
2875 async fn delete_ops_lifecycle(
2877 &self,
2878 runtime_id: &LogicalRuntimeId,
2879 ) -> Result<(), RuntimeStoreError> {
2880 let _ = runtime_id;
2881 Err(RuntimeStoreError::Unsupported(
2882 "delete_ops_lifecycle".into(),
2883 ))
2884 }
2885
2886 async fn load_mob_host_binding(
2898 &self,
2899 mob_id: &str,
2900 ) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2901 let _ = mob_id;
2902 Err(RuntimeStoreError::Unsupported(
2903 "load_mob_host_binding".into(),
2904 ))
2905 }
2906
2907 async fn list_mob_host_bindings(&self) -> Result<Vec<(String, Vec<u8>)>, RuntimeStoreError> {
2909 Err(RuntimeStoreError::Unsupported(
2910 "list_mob_host_bindings".into(),
2911 ))
2912 }
2913
2914 async fn put_mob_host_binding_if_absent(
2917 &self,
2918 mob_id: &str,
2919 record_json: &[u8],
2920 ) -> Result<bool, RuntimeStoreError> {
2921 let _ = (mob_id, record_json);
2922 Err(RuntimeStoreError::Unsupported(
2923 "put_mob_host_binding_if_absent".into(),
2924 ))
2925 }
2926
2927 async fn compare_and_put_mob_host_binding(
2930 &self,
2931 mob_id: &str,
2932 expected_json: &[u8],
2933 next_json: &[u8],
2934 ) -> Result<bool, RuntimeStoreError> {
2935 let _ = (mob_id, expected_json, next_json);
2936 Err(RuntimeStoreError::Unsupported(
2937 "compare_and_put_mob_host_binding".into(),
2938 ))
2939 }
2940
2941 async fn delete_mob_host_binding(
2944 &self,
2945 mob_id: &str,
2946 expected_json: &[u8],
2947 ) -> Result<bool, RuntimeStoreError> {
2948 let _ = (mob_id, expected_json);
2949 Err(RuntimeStoreError::Unsupported(
2950 "delete_mob_host_binding".into(),
2951 ))
2952 }
2953
2954 async fn load_mob_host_revocation(
2962 &self,
2963 mob_id: &str,
2964 ) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2965 let _ = mob_id;
2966 Err(RuntimeStoreError::Unsupported(
2967 "load_mob_host_revocation".into(),
2968 ))
2969 }
2970
2971 async fn list_mob_host_revocations(&self) -> Result<Vec<(String, Vec<u8>)>, RuntimeStoreError> {
2975 Err(RuntimeStoreError::Unsupported(
2976 "list_mob_host_revocations".into(),
2977 ))
2978 }
2979
2980 async fn revoke_mob_host_binding(
2988 &self,
2989 mob_id: &str,
2990 expected_binding_json: &[u8],
2991 receipt_json: &[u8],
2992 ) -> Result<bool, RuntimeStoreError> {
2993 let _ = (mob_id, expected_binding_json, receipt_json);
2994 Err(RuntimeStoreError::Unsupported(
2995 "revoke_mob_host_binding".into(),
2996 ))
2997 }
2998}
2999
3000pub use memory::InMemoryRuntimeStore;
3001#[cfg(feature = "sqlite-store")]
3002pub use sqlite::SqliteRuntimeStore;
3003
3004#[cfg(test)]
3005mod lifecycle_record_compatibility_tests {
3006 use super::*;
3007
3008 fn operation_id(
3009 value: u128,
3010 ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
3011 meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId::from_uuid(
3012 uuid::Uuid::from_u128(value),
3013 )
3014 }
3015
3016 fn binding(seed: u8, name: &str, epoch: u64) -> SupervisorBindingReceipt {
3017 let pubkey = [seed; 32];
3018 SupervisorBindingReceipt::new(
3019 name.to_string(),
3020 meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey).as_str(),
3021 format!("inproc://{name}"),
3022 crate::comms_drain::encode_supervisor_signing_public_key(pubkey),
3023 epoch,
3024 )
3025 }
3026
3027 fn rotation(
3028 operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
3029 phase: SupervisorRotationPersistencePhase,
3030 rejection: Option<SupervisorRotationRejection>,
3031 previous: SupervisorBindingReceipt,
3032 next: SupervisorBindingReceipt,
3033 ) -> SupervisorRotationReceipt {
3034 SupervisorRotationReceipt::new(operation_id, phase, rejection, previous, next)
3035 }
3036
3037 fn snapshot(authority: SupervisorAuthoritySnapshot) -> MachineLifecycleSnapshot {
3038 MachineLifecycleSnapshot::new(
3039 RuntimeState::Idle,
3040 MachineLifecycleBindingFacts::new(None, None, None, None),
3041 authority,
3042 )
3043 }
3044
3045 fn encode_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
3046 MachineLifecycleStoreRecord::from_snapshot(snapshot)
3047 .encode()
3048 .expect("encode lifecycle snapshot")
3049 }
3050
3051 fn encode_unvalidated_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
3052 serde_json::to_vec(&MachineLifecycleSnapshotStoreWire::from(snapshot))
3053 .expect("serialize deliberately corrupt lifecycle snapshot")
3054 }
3055
3056 fn encoded_value(snapshot: &MachineLifecycleSnapshot) -> serde_json::Value {
3057 serde_json::from_slice(&encode_snapshot(snapshot)).expect("decode encoded snapshot as JSON")
3058 }
3059
3060 fn assert_decode_fails(value: serde_json::Value) {
3061 let bytes = serde_json::to_vec(&value).expect("serialize corrupt lifecycle record");
3062 assert!(
3063 decode_machine_lifecycle_store_record(&bytes).is_err(),
3064 "corrupt lifecycle record must fail closed: {value}"
3065 );
3066 }
3067
3068 #[test]
3069 fn version_one_record_without_supervisor_authority_migrates_explicitly_to_unbound() {
3070 let bytes = serde_json::to_vec(&serde_json::json!({
3071 "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3072 "runtime_state": RuntimeState::Retired,
3073 "binding": {
3074 "agent_runtime_id": "rt:session:legacy-v1",
3075 "fence_token": 19,
3076 "runtime_generation": 4,
3077 "runtime_epoch_id": "epoch-legacy-v1"
3078 }
3079 }))
3080 .expect("serialize legacy v1 lifecycle record");
3081
3082 let decoded = decode_machine_lifecycle_store_record(&bytes)
3083 .expect("valid v1 record without the additive field must decode");
3084 assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
3085 assert_eq!(
3086 decoded.supervisor_authority(),
3087 &SupervisorAuthoritySnapshot::UnboundNoReceipt
3088 );
3089 }
3090
3091 #[test]
3092 fn current_record_requires_supervisor_authority_and_unregister_progress_presence() {
3093 assert_decode_fails(serde_json::json!({
3094 "record_version": MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3095 "runtime_state": RuntimeState::Idle,
3096 "binding": {
3097 "agent_runtime_id": null,
3098 "fence_token": null,
3099 "runtime_generation": null,
3100 "runtime_epoch_id": null
3101 },
3102 "unregister_progress": null
3103 }));
3104 }
3105
3106 #[test]
3107 fn current_nullable_fields_require_presence_but_accept_explicit_null() {
3108 let unbound = snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt);
3109 let encoded = encoded_value(&unbound);
3110 assert_eq!(
3111 decode_machine_lifecycle_store_record(
3112 &serde_json::to_vec(&encoded).expect("serialize valid current record")
3113 )
3114 .expect("explicit-null current binding fields must decode"),
3115 unbound
3116 );
3117 let mut missing_progress = encoded.clone();
3118 missing_progress
3119 .as_object_mut()
3120 .expect("lifecycle record object")
3121 .remove("unregister_progress");
3122 assert_decode_fails(missing_progress);
3123
3124 for field in [
3125 "agent_runtime_id",
3126 "fence_token",
3127 "runtime_generation",
3128 "runtime_epoch_id",
3129 ] {
3130 let mut partial = encoded.clone();
3131 partial["binding"]
3132 .as_object_mut()
3133 .expect("binding object")
3134 .remove(field);
3135 assert_decode_fails(partial);
3136 }
3137 for field in ["current_run_id", "pre_run_phase"] {
3138 let mut partial = encoded.clone();
3139 partial
3140 .as_object_mut()
3141 .expect("lifecycle record object")
3142 .remove(field);
3143 assert_decode_fails(partial);
3144 }
3145
3146 let completed = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3147 operation_id(101),
3148 SupervisorRotationPersistencePhase::Completed,
3149 None,
3150 binding(30, "required-null-previous", 4),
3151 binding(31, "required-null-next", 5),
3152 )));
3153 let mut missing_rejection = encoded_value(&completed);
3154 assert!(missing_rejection["supervisor_authority"]["rotation"]["rejection"].is_null());
3155 missing_rejection["supervisor_authority"]["rotation"]
3156 .as_object_mut()
3157 .expect("rotation object")
3158 .remove("rejection");
3159 assert_decode_fails(missing_rejection);
3160 }
3161
3162 #[test]
3163 fn lossless_observation_preserves_partial_run_pair_and_nullable_lifecycle() {
3164 let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt));
3165 let run_id = RunId::new();
3166 value["runtime_state"] = serde_json::Value::Null;
3167 value["current_run_id"] = serde_json::to_value(&run_id).expect("serialize run id");
3168 value["pre_run_phase"] = serde_json::Value::Null;
3169 let bytes = serde_json::to_vec(&value).expect("serialize partial lifecycle row");
3170
3171 let MachineLifecycleObservation::Decoded { record, version } =
3172 classify_machine_lifecycle_record(&bytes)
3173 else {
3174 panic!("explicitly nullable partial runtime tuple must remain decoded");
3175 };
3176 assert_eq!(
3177 record.record_version(),
3178 MACHINE_LIFECYCLE_STORE_RECORD_VERSION
3179 );
3180 assert_eq!(record.runtime_state(), None);
3181 assert_eq!(record.run().current_run_id(), Some(&run_id));
3182 assert_eq!(record.run().pre_run_phase(), None);
3183 assert_eq!(
3184 version.as_str(),
3185 format!("sha256:{:x}", Sha256::digest(&bytes))
3186 );
3187 assert!(decode_machine_lifecycle_store_record(&bytes).is_err());
3188 }
3189
3190 #[test]
3191 fn lifecycle_observation_distinguishes_unsupported_and_malformed_raw_rows() {
3192 let unsupported = br#"{"record_version":99,"opaque":"future"}"#;
3193 assert!(matches!(
3194 classify_machine_lifecycle_record(unsupported),
3195 MachineLifecycleObservation::Unsupported {
3196 record_version: 99,
3197 ..
3198 }
3199 ));
3200
3201 let malformed = br#"{"record_version":4,"binding":"torn"}"#;
3202 assert!(matches!(
3203 classify_machine_lifecycle_record(malformed),
3204 MachineLifecycleObservation::Malformed {
3205 record_version: Some(4),
3206 ..
3207 }
3208 ));
3209
3210 let undecodable = b"not-json";
3211 assert!(matches!(
3212 classify_machine_lifecycle_record(undecodable),
3213 MachineLifecycleObservation::Malformed {
3214 record_version: None,
3215 ..
3216 }
3217 ));
3218 }
3219
3220 #[test]
3221 fn version_three_unregister_record_migrates_without_run_binding() {
3222 let expected = snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt);
3223 let mut value = encoded_value(&expected);
3224 value["record_version"] =
3225 serde_json::json!(UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION);
3226 value
3227 .as_object_mut()
3228 .expect("lifecycle record object")
3229 .remove("current_run_id");
3230 value
3231 .as_object_mut()
3232 .expect("lifecycle record object")
3233 .remove("pre_run_phase");
3234 let bytes = serde_json::to_vec(&value).expect("serialize v3 row");
3235 let decoded = decode_machine_lifecycle_store_record(&bytes).expect("decode v3 row");
3236 assert_eq!(decoded, expected);
3237 assert_eq!(decoded.run(), &MachineLifecycleRunFacts::default());
3238 }
3239
3240 #[test]
3241 fn version_two_supervisor_record_migrates_with_no_unregister_progress() {
3242 let bytes = serde_json::to_vec(&serde_json::json!({
3243 "record_version": SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3244 "runtime_state": RuntimeState::Retired,
3245 "binding": {
3246 "agent_runtime_id": "rt:session:legacy-v2",
3247 "fence_token": 23,
3248 "runtime_generation": 5,
3249 "runtime_epoch_id": "epoch-legacy-v2"
3250 },
3251 "supervisor_authority": { "kind": "unbound_no_receipt" }
3252 }))
3253 .expect("serialize v2 lifecycle record");
3254
3255 let decoded = decode_machine_lifecycle_store_record(&bytes)
3256 .expect("valid v2 supervisor record must migrate");
3257 assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
3258 assert_eq!(decoded.unregister_progress(), None);
3259 }
3260
3261 #[test]
3262 fn current_unregister_progress_rejects_forced_disposition_before_feedback() {
3263 let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt));
3264 value["unregister_progress"] = serde_json::json!({
3265 "runtime_loop_drain_pending": true,
3266 "comms_drain_exit_pending": false,
3267 "completion_waiter_drain_pending": true,
3268 "runtime_loop_forced_abort": true,
3269 "comms_drain_forced_abort": false
3270 });
3271 assert_decode_fails(value);
3272 }
3273
3274 #[test]
3275 fn version_one_migration_rejects_current_authority_fields() {
3276 assert_decode_fails(serde_json::json!({
3277 "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3278 "runtime_state": RuntimeState::Idle,
3279 "binding": {
3280 "agent_runtime_id": null,
3281 "fence_token": null,
3282 "runtime_generation": null,
3283 "runtime_epoch_id": null
3284 },
3285 "supervisor_authority": { "kind": "unbound_no_receipt" }
3286 }));
3287 }
3288
3289 #[test]
3290 fn mixed_or_unknown_supervisor_authority_fields_fail_closed() {
3291 let current = binding(1, "current-supervisor", 7);
3292 let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::Bound(current)));
3293 value["supervisor_authority"]["rotation"] = serde_json::json!({});
3294 assert_decode_fails(value);
3295 }
3296
3297 #[test]
3298 fn completed_rotation_operation_receipt_round_trips_for_cold_observation() {
3299 let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3300 operation_id(1),
3301 SupervisorRotationPersistencePhase::Completed,
3302 None,
3303 binding(1, "previous-supervisor", 7),
3304 binding(2, "next-supervisor", 8),
3305 )));
3306
3307 let encoded = encode_snapshot(&snapshot);
3308 let decoded = decode_machine_lifecycle_store_record(&encoded)
3309 .expect("decode completed rotation receipt");
3310
3311 assert_eq!(decoded, snapshot);
3312 }
3313
3314 #[test]
3315 fn exact_current_completed_adoption_round_trips_but_other_equal_epoch_completion_fails() {
3316 let current = binding(3, "already-rotated-supervisor", 9);
3317 let adoption = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3318 operation_id(2),
3319 SupervisorRotationPersistencePhase::Completed,
3320 None,
3321 current.clone(),
3322 current,
3323 )));
3324 assert_eq!(
3325 decode_machine_lifecycle_store_record(&encode_snapshot(&adoption))
3326 .expect("exact-current legacy adoption receipt must decode"),
3327 adoption
3328 );
3329
3330 let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3331 operation_id(3),
3332 SupervisorRotationPersistencePhase::Completed,
3333 None,
3334 binding(3, "previous-supervisor", 9),
3335 binding(4, "different-supervisor", 9),
3336 )));
3337 assert!(
3338 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&non_advancing))
3339 .is_err()
3340 );
3341 }
3342
3343 #[test]
3344 fn malformed_rotation_descriptors_epochs_and_operation_ids_fail_closed() {
3345 let invalid_previous = SupervisorBindingReceipt::new(
3346 String::new(),
3347 "not-a-uuid".to_string(),
3348 "not-an-address".to_string(),
3349 "not-a-key".to_string(),
3350 1,
3351 );
3352 let invalid_previous_receipt =
3353 snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3354 operation_id(4),
3355 SupervisorRotationPersistencePhase::Rejected,
3356 Some(SupervisorRotationRejection::InvalidTarget),
3357 invalid_previous,
3358 binding(5, "raw-target", 2),
3359 )));
3360 assert!(
3361 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3362 &invalid_previous_receipt,
3363 ))
3364 .is_err()
3365 );
3366
3367 let invalid_next = SupervisorBindingReceipt::new(
3368 "invalid-target".to_string(),
3369 "not-a-uuid".to_string(),
3370 "not-an-address".to_string(),
3371 "not-a-key".to_string(),
3372 2,
3373 );
3374 let invalid_completed_target =
3375 snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3376 operation_id(5),
3377 SupervisorRotationPersistencePhase::Completed,
3378 None,
3379 binding(6, "previous-supervisor", 1),
3380 invalid_next,
3381 )));
3382 assert!(
3383 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3384 &invalid_completed_target,
3385 ))
3386 .is_err()
3387 );
3388
3389 let mut invalid_id = encoded_value(&snapshot(
3390 SupervisorAuthoritySnapshot::RotationOperation(rotation(
3391 operation_id(6),
3392 SupervisorRotationPersistencePhase::PreviousRevokePending,
3393 None,
3394 binding(7, "previous-supervisor", 1),
3395 binding(8, "next-supervisor", 2),
3396 )),
3397 ));
3398 invalid_id["supervisor_authority"]["rotation"]["operation_id"] =
3399 serde_json::json!("not-a-uuid");
3400 assert_decode_fails(invalid_id);
3401
3402 let nil_id = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3403 operation_id(0),
3404 SupervisorRotationPersistencePhase::PreviousRevokePending,
3405 None,
3406 binding(7, "previous-supervisor", 1),
3407 binding(8, "next-supervisor", 2),
3408 )));
3409 assert!(
3410 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&nil_id)).is_err()
3411 );
3412
3413 let non_advancing_pending =
3414 snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3415 operation_id(13),
3416 SupervisorRotationPersistencePhase::PreviousRevokePending,
3417 None,
3418 binding(7, "previous-supervisor", 4),
3419 binding(8, "next-supervisor", 4),
3420 )));
3421 assert!(
3422 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3423 &non_advancing_pending,
3424 ))
3425 .is_err()
3426 );
3427 }
3428
3429 #[test]
3430 fn rejected_invalid_or_unsupported_target_preserves_raw_evidence() {
3431 for (id, rejection) in [
3432 (7, SupervisorRotationRejection::InvalidTarget),
3433 (14, SupervisorRotationRejection::UnsupportedProtocolVersion),
3434 ] {
3435 let raw_invalid_target = SupervisorBindingReceipt::new(
3436 "".to_string(),
3437 "not-a-peer-id".to_string(),
3438 "not-an-address".to_string(),
3439 "not-a-signing-key".to_string(),
3440 0,
3441 );
3442 let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3443 operation_id(id),
3444 SupervisorRotationPersistencePhase::Rejected,
3445 Some(rejection),
3446 binding(9, "retained-supervisor", 11),
3447 raw_invalid_target,
3448 )));
3449 assert_eq!(
3450 decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
3451 .expect("rejected raw target evidence must remain durable"),
3452 snapshot
3453 );
3454 }
3455 }
3456
3457 #[test]
3458 fn only_raw_target_rejections_are_durable_and_epoch_rejection_must_be_genuine() {
3459 for (id, rejection) in [
3460 (102, SupervisorRotationRejection::OperationConflict),
3461 (103, SupervisorRotationRejection::NotBound),
3462 (104, SupervisorRotationRejection::SenderMismatch),
3463 ] {
3464 let impossible = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3465 operation_id(id),
3466 SupervisorRotationPersistencePhase::Rejected,
3467 Some(rejection),
3468 binding(32, "retained-supervisor", 7),
3469 binding(33, "requested-supervisor", 8),
3470 )));
3471 assert!(
3472 MachineLifecycleStoreRecord::from_snapshot(&impossible)
3473 .encode()
3474 .is_err()
3475 );
3476 assert!(
3477 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&impossible))
3478 .is_err()
3479 );
3480 }
3481
3482 let advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3483 operation_id(105),
3484 SupervisorRotationPersistencePhase::Rejected,
3485 Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3486 binding(34, "retained-supervisor", 9),
3487 binding(35, "advancing-target", 10),
3488 )));
3489 assert!(
3490 MachineLifecycleStoreRecord::from_snapshot(&advancing)
3491 .encode()
3492 .is_err()
3493 );
3494 assert!(
3495 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&advancing))
3496 .is_err()
3497 );
3498
3499 let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3500 operation_id(106),
3501 SupervisorRotationPersistencePhase::Rejected,
3502 Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3503 binding(36, "retained-supervisor", 11),
3504 binding(37, "non-advancing-target", 11),
3505 )));
3506 assert_eq!(
3507 decode_machine_lifecycle_store_record(&encode_snapshot(&non_advancing))
3508 .expect("genuine target-epoch rejection must remain durable"),
3509 non_advancing
3510 );
3511 }
3512
3513 #[test]
3514 fn malformed_current_authority_variants_fail_closed() {
3515 let malformed = SupervisorBindingReceipt::new(
3516 String::new(),
3517 "not-a-peer-id".to_string(),
3518 "not-an-address".to_string(),
3519 "not-a-signing-key".to_string(),
3520 1,
3521 );
3522 let bound = snapshot(SupervisorAuthoritySnapshot::Bound(malformed.clone()));
3523 assert!(
3524 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&bound)).is_err()
3525 );
3526
3527 let pending = snapshot(SupervisorAuthoritySnapshot::RevocationPending(
3528 SupervisorRevocationPendingReceipt::new(
3529 malformed.name().to_owned(),
3530 malformed.peer_id().to_owned(),
3531 malformed.address().to_owned(),
3532 malformed.signing_public_key().to_owned(),
3533 malformed.epoch(),
3534 ),
3535 ));
3536 assert!(
3537 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&pending)).is_err()
3538 );
3539
3540 let revoked = snapshot(SupervisorAuthoritySnapshot::RevokedReceipt(
3541 RevokedSupervisorReceipt::new(
3542 malformed.peer_id().to_owned(),
3543 malformed.signing_public_key().to_owned(),
3544 malformed.epoch(),
3545 ),
3546 ));
3547 assert!(
3548 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&revoked)).is_err()
3549 );
3550 }
3551
3552 #[test]
3553 fn partial_and_nonterminal_history_records_fail_closed() {
3554 let receipt = rotation(
3555 operation_id(8),
3556 SupervisorRotationPersistencePhase::Completed,
3557 None,
3558 binding(10, "history-previous", 1),
3559 binding(11, "history-next", 2),
3560 );
3561 let history = std::collections::BTreeMap::from([(receipt.operation_id(), receipt)]);
3562 let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3563 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3564 12,
3565 "current-supervisor",
3566 3,
3567 ))),
3568 terminal_receipts: history,
3569 });
3570
3571 let mut partial = encoded_value(&snapshot);
3572 partial["supervisor_authority"]["terminal_receipts"][0]
3573 .as_object_mut()
3574 .expect("history receipt object")
3575 .remove("next");
3576 assert_decode_fails(partial);
3577
3578 let mut nonterminal = encoded_value(&snapshot);
3579 nonterminal["supervisor_authority"]["terminal_receipts"][0]["phase"] =
3580 serde_json::json!("next_publish_pending");
3581 assert_decode_fails(nonterminal);
3582 }
3583
3584 #[test]
3585 fn duplicate_nested_and_active_history_conflicts_fail_closed() {
3586 let history_receipt = rotation(
3587 operation_id(9),
3588 SupervisorRotationPersistencePhase::Completed,
3589 None,
3590 binding(13, "history-previous", 1),
3591 binding(14, "history-next", 2),
3592 );
3593 let history = std::collections::BTreeMap::from([(
3594 history_receipt.operation_id(),
3595 history_receipt.clone(),
3596 )]);
3597 let wrapper = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3598 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3599 15,
3600 "current-supervisor",
3601 3,
3602 ))),
3603 terminal_receipts: history,
3604 });
3605
3606 let mut duplicate = encoded_value(&wrapper);
3607 let receipt = duplicate["supervisor_authority"]["terminal_receipts"][0].clone();
3608 duplicate["supervisor_authority"]["terminal_receipts"]
3609 .as_array_mut()
3610 .expect("history receipt array")
3611 .push(receipt);
3612 assert_decode_fails(duplicate);
3613
3614 let mut nested = encoded_value(&wrapper);
3615 let nested_current = nested["supervisor_authority"].clone();
3616 nested["supervisor_authority"]["current"] = nested_current;
3617 assert_decode_fails(nested);
3618
3619 let active_conflict = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3620 current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(
3621 history_receipt.clone(),
3622 )),
3623 terminal_receipts: std::collections::BTreeMap::from([(
3624 history_receipt.operation_id(),
3625 history_receipt,
3626 )]),
3627 });
3628 assert!(
3629 MachineLifecycleStoreRecord::from_snapshot(&active_conflict)
3630 .encode()
3631 .is_err()
3632 );
3633
3634 let empty_history = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3635 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3636 20,
3637 "current-supervisor",
3638 4,
3639 ))),
3640 terminal_receipts: std::collections::BTreeMap::new(),
3641 });
3642 assert!(
3643 MachineLifecycleStoreRecord::from_snapshot(&empty_history)
3644 .encode()
3645 .is_err()
3646 );
3647 assert!(
3648 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&empty_history))
3649 .is_err()
3650 );
3651
3652 let mismatched_key = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3653 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3654 21,
3655 "current-supervisor",
3656 4,
3657 ))),
3658 terminal_receipts: std::collections::BTreeMap::from([(
3659 operation_id(99),
3660 rotation(
3661 operation_id(98),
3662 SupervisorRotationPersistencePhase::Completed,
3663 None,
3664 binding(22, "history-previous", 2),
3665 binding(23, "history-next", 3),
3666 ),
3667 )]),
3668 });
3669 assert!(
3670 MachineLifecycleStoreRecord::from_snapshot(&mismatched_key)
3671 .encode()
3672 .is_err()
3673 );
3674 }
3675
3676 #[test]
3677 fn history_current_epoch_and_same_epoch_identity_must_cohere() {
3678 let previous = binding(38, "history-previous", 12);
3679 let next = binding(39, "history-next", 13);
3680 let completed = rotation(
3681 operation_id(107),
3682 SupervisorRotationPersistencePhase::Completed,
3683 None,
3684 previous.clone(),
3685 next.clone(),
3686 );
3687 let history =
3688 std::collections::BTreeMap::from([(completed.operation_id(), completed.clone())]);
3689
3690 let stale_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3691 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3692 38,
3693 "refreshed-history-previous",
3694 12,
3695 ))),
3696 terminal_receipts: history.clone(),
3697 });
3698 assert!(
3699 MachineLifecycleStoreRecord::from_snapshot(&stale_current)
3700 .encode()
3701 .is_err()
3702 );
3703 assert!(
3704 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&stale_current))
3705 .is_err()
3706 );
3707
3708 let conflicting_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3709 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3710 40,
3711 "conflicting-current",
3712 13,
3713 ))),
3714 terminal_receipts: history.clone(),
3715 });
3716 assert!(
3717 MachineLifecycleStoreRecord::from_snapshot(&conflicting_current)
3718 .encode()
3719 .is_err()
3720 );
3721 assert!(
3722 decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3723 &conflicting_current,
3724 ))
3725 .is_err()
3726 );
3727
3728 let route_refreshed_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3729 current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3730 39,
3731 "route-refreshed-history-next",
3732 13,
3733 ))),
3734 terminal_receipts: history,
3735 });
3736 assert_eq!(
3737 decode_machine_lifecycle_store_record(&encode_snapshot(&route_refreshed_current))
3738 .expect("same identity may refresh route metadata within one epoch"),
3739 route_refreshed_current
3740 );
3741 }
3742
3743 #[test]
3744 fn terminal_history_survives_later_rotation_and_recovery() {
3745 let first = rotation(
3746 operation_id(10),
3747 SupervisorRotationPersistencePhase::Completed,
3748 None,
3749 binding(16, "first-supervisor", 1),
3750 binding(17, "second-supervisor", 2),
3751 );
3752 let rejected = rotation(
3753 operation_id(11),
3754 SupervisorRotationPersistencePhase::Rejected,
3755 Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3756 binding(17, "second-supervisor", 2),
3757 binding(18, "rejected-supervisor", 2),
3758 );
3759 let later = rotation(
3760 operation_id(12),
3761 SupervisorRotationPersistencePhase::Completed,
3762 None,
3763 binding(17, "second-supervisor", 2),
3764 binding(19, "current-supervisor", 3),
3765 );
3766 let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3767 current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(later)),
3768 terminal_receipts: std::collections::BTreeMap::from([
3769 (first.operation_id(), first),
3770 (rejected.operation_id(), rejected),
3771 ]),
3772 });
3773
3774 let decoded = decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
3775 .expect("later rotation and old terminal history must recover together");
3776 assert_eq!(decoded, snapshot);
3777 }
3778}