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