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