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