Skip to main content

meerkat_runtime/store/
mod.rs

1//! RuntimeStore — atomic persistence for runtime state.
2//!
3//! Machine-owned runtime commands durably persist [`RunBoundaryReceipt`] values
4//! atomically with their session and input-state effects.
5
6pub mod memory;
7#[cfg(feature = "sqlite-store")]
8pub mod sqlite;
9
10use 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
24/// Maximum number of exact input-state rows admitted by one compare-and-swap
25/// boundary. Directed-terminal outbox batches share the same 256-row bound as
26/// their publication seam.
27pub const MAX_INPUT_STATE_BATCH_CAS: usize = 256;
28
29/// Result of an exact input-state batch compare-and-swap.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InputStateBatchCasOutcome {
32    /// Every expected durable row matched and every replacement committed, or
33    /// every durable row was already byte-identical to its replacement from an
34    /// earlier invocation whose acknowledgement was lost.
35    Swapped,
36    /// At least one expected row was missing or no longer byte-identical; no
37    /// replacement was written.
38    Stale,
39}
40
41/// Result of an exact input-state batch compare-and-swap performed under an
42/// external authority fence.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum FencedInputStateBatchCasOutcome {
45    /// The target rows matched and the replacements committed, or were already
46    /// byte-identical, while the external authority was current.
47    Swapped,
48    /// At least one target row no longer matched. The external fence was not
49    /// consulted and no replacement was written.
50    Stale,
51    /// The external authority was superseded. No replacement was written.
52    FenceConflict { reason: String },
53    /// The external authority could not be checked temporarily. No replacement
54    /// was written.
55    FenceBackoff { reason: String },
56}
57
58#[derive(Debug)]
59struct PreparedInputStateBatchCasRow {
60    input_id: InputId,
61    expected_json: Vec<u8>,
62    replacement: StoredInputState,
63    // SQLite writes the already-validated bytes; the in-memory implementation
64    // stores the typed replacement directly.
65    #[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/// Errors from RuntimeStore operations.
142#[derive(Debug, Clone, thiserror::Error)]
143#[non_exhaustive]
144pub enum RuntimeStoreError {
145    /// Write failed.
146    #[error("Store write failed: {0}")]
147    WriteFailed(String),
148    /// Read failed.
149    #[error("Store read failed: {0}")]
150    ReadFailed(String),
151    /// The explicit session-store key does not match the serialized session.
152    #[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    /// Not found.
158    #[error("Not found: {0}")]
159    NotFound(String),
160    /// Operation is not supported by this store implementation.
161    #[error("Unsupported store operation: {0}")]
162    Unsupported(String),
163    /// A detached producer attempted to persist an ops snapshot after the
164    /// matching epoch was atomically retired by unregister.
165    #[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    /// An unregister-finalization commit may have become durable, but the
171    /// backend could not authoritatively classify its outcome.
172    ///
173    /// Callers must retry the idempotent atomic finalization and must not
174    /// publish a compensating lifecycle rollback for this error.
175    #[error("Unregister finalization outcome is unknown: {0}")]
176    UnregisterFinalizationOutcomeUnknown(String),
177    /// Runtime snapshot CAS rejected a stale transcript rewrite.
178    #[error("Transcript revision conflict: expected {expected}, actual {actual}")]
179    TranscriptRevisionConflict { expected: String, actual: String },
180    /// An atomic boundary commit carried a session snapshot that was already
181    /// superseded by the durable append-only head. Callers must observe this
182    /// as a failed commit rather than mistaking a no-op for publication.
183    #[error("Session snapshot for runtime '{runtime_id}' was superseded by the durable head")]
184    SessionSnapshotSuperseded { runtime_id: String },
185    /// The requested exact input-state batch CAS has an invalid row/key shape.
186    #[error("Invalid input-state batch compare-and-swap: {reason}")]
187    InvalidInputStateBatchCas { reason: String },
188    /// A lifecycle record was observed exactly, but replacing it would risk
189    /// lowering or fabricating durable runtime fencing authority.
190    ///
191    /// This is a permanent reconciliation result for the observed row, not a
192    /// transport retry. Callers should project RepairBlocked while retaining
193    /// the evidence digest for operator repair.
194    #[error("Machine lifecycle repair is blocked: {detail}")]
195    MachineLifecycleRepairBlocked {
196        evidence_digest: Option<String>,
197        detail: String,
198    },
199    /// The file's schema ledger records a version newer than this binary
200    /// supports: a newer binary migrated the file and this one must refuse
201    /// it (typed, health-visible refusal — never a crash loop).
202    #[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    /// The exclusive maintenance fence is held for this database; storage is
212    /// under offline maintenance.
213    #[error("maintenance fence is held for '{path}'; storage is under offline maintenance")]
214    MaintenanceFenceHeld { path: String },
215    /// Internal error.
216    #[error("Internal error: {0}")]
217    Internal(String),
218}
219
220/// Transactional updater for the runtime-owned OAuth login-flow payload snapshot.
221pub type AuthOAuthFlowSnapshotUpdate<'a> =
222    dyn FnMut(Option<&[u8]>) -> Result<Vec<u8>, RuntimeStoreError> + 'a;
223
224/// Describes a serialized session snapshot for boundary and snapshot-only commits.
225#[derive(Debug, Clone)]
226pub struct SessionDelta {
227    /// Serialized session snapshot (opaque to RuntimeStore).
228    pub session_snapshot: Vec<u8>,
229}
230
231/// Opaque generated runtime-delivery authority persisted by a
232/// [`RuntimeStore`].
233///
234/// Stores compare the mechanical revision and retain the bytes exactly. They
235/// do not interpret delivery lifecycle, sequence assignment, or cursor
236/// semantics.
237#[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/// Opaque runtime-inbox row committed alongside generated delivery authority.
262#[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/// Mechanical compare-and-swap result for runtime-delivery authority.
297#[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
311/// Clear one finalized compaction intent while preserving typed checkpoint
312/// custody for the exact replacement document.
313///
314/// Legacy snapshots retain their legacy-unverified classification; callers do
315/// not gain typed authority merely by finalizing an outbox row. A verified
316/// snapshot advances by one exact run-boundary successor so the persisted
317/// document can never carry its predecessor's now-stale digest.
318pub(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/// Runtime binding facts selected by generated MeerkatMachine authority.
354///
355/// RuntimeStore implementations persist and read these facts as part of a
356/// machine lifecycle snapshot. The commit token that writes these facts stays
357/// crate-private so compatibility callers cannot mint replacement lifecycle
358/// truth.
359#[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/// Durable identity receipt for the last completed supervisor revoke.
368///
369/// This is not a live supervisor binding and carries no route address. It is
370/// only the identity/key/epoch witness needed to authorize an exact duplicate
371/// revoke response after a cold restart; the current authenticated request
372/// supplies its current route.
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct RevokedSupervisorReceipt {
375    peer_id: String,
376    signing_public_key: String,
377    epoch: u64,
378}
379
380/// Durable current supervisor binding used to authenticate terminal retry
381/// traffic after a cold runtime restart.
382#[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/// Durable in-flight supervisor revocation receipt.
392///
393/// This is the closed-world hand-off between generated machine authority and
394/// the concrete router mutation.  It deliberately retains the complete prior
395/// route so a cold runtime can authenticate an exact retry and re-materialize
396/// the generated remove obligation without resurrecting a live binding.
397#[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/// Closed durable supervisor authority state. Each variant owns one complete
574/// recovery shape; terminal rotation receipts retain their exact operation and
575/// participant descriptors for idempotent submission and later observation.
576#[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/// Exact content version of one observed machine-lifecycle row.
626///
627/// The version is the domain-prefixed SHA-256 digest of the raw stored bytes,
628/// not a decoded projection. It therefore remains a valid target-local CAS
629/// witness for unsupported and malformed rows as well as current records.
630#[derive(Debug, Clone, PartialEq, Eq, Hash)]
631pub struct MachineLifecycleObservationVersion(String);
632
633impl MachineLifecycleObservationVersion {
634    /// Derive the exact target-local compare token for opaque stored bytes.
635    ///
636    /// Custom [`RuntimeStore`] implementations use the same constructor for
637    /// both observations and successful CAS receipts; no decoded lifecycle
638    /// shape is allowed to stand in for the physical row version.
639    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/// Independently observed run-binding atoms.
650///
651/// A torn row may contain exactly one side of this pair. The store preserves
652/// that shape; the generated reconciler, not the decoder, decides whether it
653/// can be normalized.
654#[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/// Decoded runtime-lifecycle observation.
691///
692/// The lifecycle phase, four binding atoms, and two run atoms remain
693/// independently optional. This type deliberately represents partial tuples
694/// such as `current_run_id = Some` with `pre_run_phase = None` instead of
695/// rejecting them as an impossible transition shape.
696#[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/// Lossless classification of one physical machine-lifecycle row.
739///
740/// Transport failures remain [`RuntimeStoreError`] values. Every successfully
741/// read row is classified without collapsing unsupported or corrupt bytes into
742/// absence.
743#[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    /// Losslessly classify one successfully read physical lifecycle row.
765    ///
766    /// This is the canonical adapter seam for custom stores. Transport
767    /// failure stays an outer [`RuntimeStoreError`]; every byte sequence read
768    /// successfully becomes Decoded, Unsupported, or Malformed here.
769    #[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/// Target-local precondition for lifecycle normalization.
799#[derive(Debug, Clone, PartialEq, Eq)]
800pub enum MachineLifecycleExpectedVersion {
801    Missing,
802    Version(MachineLifecycleObservationVersion),
803}
804
805impl MachineLifecycleObservation {
806    /// Exact target-local precondition represented by this observation.
807    ///
808    /// Missing is a first-class compare value. Every present row, including
809    /// unsupported and malformed bytes, is compared by its raw-content
810    /// version rather than by a decoded projection.
811    #[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/// Result of executing one synchronous target write under an external fence.
821///
822/// The fence is deliberately runtime-generic. A caller may back it with a
823/// lease, process-incarnation lock, or another authority source without the
824/// runtime store depending on that owner's domain types.
825#[derive(Debug, Clone, PartialEq, Eq)]
826pub enum RuntimeStoreWriteFenceOutcome {
827    /// The fence was current and invoked the supplied operation exactly once.
828    Applied,
829    /// Durable authority was superseded. The operation was not invoked.
830    Conflict { reason: String },
831    /// Authority could not be checked temporarily. The operation was not
832    /// invoked and the caller should retry after re-observation.
833    Backoff { reason: String },
834}
835
836/// Synchronous authority guard for a RuntimeStore target write.
837///
838/// Implementations MUST retain their authority serialization guard for the
839/// full duration of `operation`, invoke it exactly once only when authority is
840/// current, and never invoke it for Conflict or Backoff. The operation is
841/// synchronous by design so built-in stores can call it inside their own lock
842/// or transaction immediately before the target write. Time-bounded authority
843/// must be evaluated using the authority store's own clock while that guard is
844/// held, never a caller-supplied observation timestamp. Once a successful
845/// operation returns, the fence must return Applied without a new fallible
846/// boundary. Implementations must not re-enter the same RuntimeStore from this
847/// callback.
848pub 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/// Result of a target-local lifecycle CAS performed under an external fence.
890///
891/// Applied and AlreadyExact carry the exact decoded row used to construct the
892/// fresh process-local registration. Callers never receive or construct a
893/// MachineLifecycleCommit.
894#[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/// Result of a target-local lifecycle compare-and-swap.
916#[derive(Debug, Clone, PartialEq, Eq)]
917pub enum MachineLifecycleCasOutcome {
918    Applied {
919        version: MachineLifecycleObservationVersion,
920    },
921    Conflict {
922        current: MachineLifecycleObservation,
923    },
924}
925
926/// Durable read-back shape for machine-owned lifecycle state.
927#[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/// Durable generated unregister-saga progress needed to resume an interrupted
937/// Draining epoch without reconstructing missing producer outcomes in shell
938/// code.
939#[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    /// Runtime state selected by the owning MeerkatMachine transition.
1027    pub fn runtime_state(&self) -> RuntimeState {
1028        self.runtime_state
1029    }
1030
1031    /// Runtime binding facts selected by the owning MeerkatMachine transition.
1032    pub fn binding(&self) -> &MachineLifecycleBindingFacts {
1033        &self.binding
1034    }
1035
1036    /// Independently persisted run-binding atoms.
1037    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/// Exact pre-supervisor-authority lifecycle shape. Version 1 is decoded only
1253/// through this migration carrier so a missing authority on a current record
1254/// cannot be confused with legacy data.
1255#[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            // A legacy member may already have the exact target installed
1584            // before the operation protocol assigns an id. Its adoption
1585            // receipt is Completed with an exact previous == next witness.
1586            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                    // These two rejection classes retain the undecodable target
1609                    // fields as raw evidence. They are deliberately exempt from
1610                    // target descriptor validation and epoch comparison.
1611                }
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) = &current
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
2162/// Validate whether an exact lifecycle observation may be normalized.
2163///
2164/// Binding, fence, generation, and run atoms describe the dead process that
2165/// authored the observed row; they are not a durable high-water authority and
2166/// may be cleared by an exact-version cold-normalization CAS. Unsupported and
2167/// malformed rows remain fail-closed because this slice cannot prove their
2168/// custody fields safe to preserve.
2169fn 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    /// A runtime-authority normalization owns only lifecycle, binding, and run
2207    /// atoms. Preserve independent supervisor and unregister custody from the
2208    /// exact decoded row rather than copying it through the reconciler.
2209    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
2246/// Load the last persisted runtime-state projection from a generated lifecycle
2247/// record.
2248///
2249/// This is a projection of [`MachineLifecycleCommit`] authority. Store
2250/// implementations provide only opaque record bytes; the runtime crate owns the
2251/// decoding and rejects compatibility rows that are not machine lifecycle
2252/// records.
2253pub 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/// Declared durable store record for generated machine lifecycle truth.
2274///
2275/// Stores receive this record from [`MachineLifecycleCommit`] and may persist
2276/// its encoded form. Loading must decode this exact record shape; compatibility
2277/// runtime-state projections are not lifecycle authority.
2278#[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/// Machine-owned lifecycle commit token.
2301///
2302/// This token has no public constructor. RuntimeStore implementors can persist
2303/// the selected state and binding facts, but callers outside the machine/driver
2304/// commit path cannot select arbitrary lifecycle truth.
2305#[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    /// Runtime state selected by the owning MeerkatMachine transition.
2359    pub fn runtime_state(&self) -> RuntimeState {
2360        self.snapshot.runtime_state()
2361    }
2362
2363    /// Durable lifecycle snapshot selected by the owning MeerkatMachine transition.
2364    pub fn snapshot(&self) -> &MachineLifecycleSnapshot {
2365        &self.snapshot
2366    }
2367
2368    /// Durable record selected by the owning MeerkatMachine transition.
2369    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/// Machine-authorized final-unregister persistence token.
2379///
2380/// The token bundles terminal lifecycle truth with the exact authorized input
2381/// snapshot. It has no public constructor and can only be minted by consuming
2382/// the private-field delete witness derived from the generated
2383/// `DeleteSnapshot` unregister verdict.
2384#[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    /// Opaque encoded lifecycle record selected by final-unregister machine
2420    /// authority. External stores can persist this without gaining a way to
2421    /// construct or alter the authority token.
2422    pub fn lifecycle_store_record(&self) -> MachineLifecycleStoreRecord {
2423        self.machine_lifecycle.store_record()
2424    }
2425
2426    /// Authorized input-state rows that must commit in the same transaction.
2427    pub fn input_states(&self) -> &[InputStatePersistenceRecord] {
2428        &self.input_states
2429    }
2430
2431    /// Exact ops epoch retired by this finalization transaction.
2432    pub fn retired_ops_epoch(&self) -> &meerkat_core::RuntimeEpochId {
2433        &self.retired_ops_epoch
2434    }
2435}
2436
2437/// One durable input row observed by [`RuntimeStore::load_input_states`].
2438#[derive(Debug, Clone)]
2439pub enum InputStateRow {
2440    /// The row decoded under this binary's persisted contract.
2441    Decoded(Box<StoredInputState>),
2442    /// The row's persisted bytes no longer decode. The row stays on disk
2443    /// untouched (forensics), and is reported typed so one damaged row does
2444    /// not make the runtime's other durable inputs unreadable.
2445    Corrupt {
2446        /// Row key as stored (the row's JSON no longer parses, so the typed
2447        /// `InputId` cannot be recovered from it).
2448        input_id: String,
2449        /// Decode failure detail.
2450        detail: String,
2451    },
2452}
2453
2454/// Recovery projection of [`RuntimeStore::load_input_states`]: corrupt rows
2455/// are reported loudly and skipped so one damaged row cannot make the whole
2456/// runtime unrecoverable (the v0.8.7 failure mode). The damaged rows stay on
2457/// disk untouched for forensics.
2458pub 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/// Atomic persistence interface for runtime state.
2480///
2481/// Implementations:
2482/// - `InMemoryRuntimeStore` — in-memory, no durability (ephemeral/testing)
2483/// - `SqliteRuntimeStore` — SQLite-backed durable runtime state
2484///
2485/// A store may contain many logical runtime ids, but each id is controlled by
2486/// one live `MeerkatMachine` authority. Store transactions provide durable
2487/// atomicity; they are not a distributed lease for two machines concurrently
2488/// controlling the same logical runtime.
2489#[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    /// Whether [`RuntimeStore::atomic_apply`] durably records typed compaction
2493    /// projection intents in the same boundary as the session rewrite.
2494    /// Unknown/custom stores fail closed by default.
2495    fn supports_compaction_projection_outbox(&self) -> bool {
2496        false
2497    }
2498
2499    /// Stable key for process-local auth/OAuth authority reuse across reopened
2500    /// handles for the same durable store.
2501    fn auth_authority_key(&self) -> Option<String> {
2502        None
2503    }
2504
2505    /// Load the exact generated runtime-delivery authority record.
2506    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    /// Load one durable runtime-delivery inbox row by stable identity.
2517    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    /// Compare-and-swap generated delivery authority and optionally insert one
2529    /// inbox row in the same atomic boundary.
2530    ///
2531    /// `expected_revision = None` means the authority row must be absent.
2532    /// Stores enforce only exact CAS, row uniqueness, and atomicity; the
2533    /// generated machine decides sequence allocation and application order.
2534    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    /// List durable inbox rows in generated sequence order.
2553    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    /// Persist the runtime-owned OAuth login-flow payload snapshot.
2566    ///
2567    /// The AuthMachine owns admission/consume semantics; this payload snapshot
2568    /// carries the PKCE verifier and device-code correlation data needed to
2569    /// rehydrate active flows after a persistent runtime process restart.
2570    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    /// Load the runtime-owned OAuth login-flow payload snapshot, if present.
2581    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    /// Atomically update the runtime-owned OAuth login-flow payload snapshot.
2588    ///
2589    /// Stores that support OAuth snapshots must override this with a lock,
2590    /// transaction, or compare-and-swap boundary. A load/compute/persist
2591    /// fallback is not safe for admission, capacity, or consume claims.
2592    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    /// Atomically persist a session snapshot that is not a run boundary.
2602    ///
2603    /// Session-control snapshots update durable session authority without
2604    /// producing a [`RunBoundaryReceipt`].
2605    async fn commit_session_snapshot(
2606        &self,
2607        runtime_id: &LogicalRuntimeId,
2608        session_delta: SessionDelta,
2609    ) -> Result<(), RuntimeStoreError>;
2610
2611    /// Atomically persist a same-session transcript rewrite snapshot.
2612    ///
2613    /// Store implementations that support transcript edits must compare the
2614    /// currently persisted session transcript revision with `commit.parent_revision`
2615    /// inside the same lock or transaction that writes `session_delta`.
2616    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    /// Atomically persist session delta + receipt + input state updates.
2629    ///
2630    /// All three writes MUST commit in a single atomic operation.
2631    /// If any write fails, none should be visible.
2632    /// Atomically persist session delta + receipt + input state updates.
2633    ///
2634    /// All writes MUST commit in a single atomic operation.
2635    /// If `session_store_key` is `Some`, validates that the snapshot belongs
2636    /// to that session and, for stores that physically share a `SessionStore`
2637    /// table, writes that table in the same transaction. Runtime snapshot
2638    /// authority remains keyed only by `runtime_id`; `session_store_key` must
2639    /// not create a raw session UUID runtime alias.
2640    /// Compaction intents must be inserted as pending outbox rows in this same
2641    /// boundary. An intent whose exact outbox identity is already finalized is
2642    /// a stale snapshot replay and must be rejected without mutating any part
2643    /// of the boundary.
2644    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    /// Load exact compaction projection intents committed by atomic_apply but
2654    /// not yet acknowledged as finalized by the memory store.
2655    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    /// Idempotently acknowledge post-commit memory finalization.
2666    ///
2667    /// The acknowledgement and removal of this exact intent from the
2668    /// authoritative persisted session snapshot MUST occur in one atomic
2669    /// boundary. The finalized outbox row remains as a tombstone so later
2670    /// snapshot writes can reject stale metadata replay.
2671    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    /// Atomically persist a failed-but-applied runtime turn.
2683    ///
2684    /// This is the machine-terminal counterpart to [`Self::atomic_apply`]:
2685    /// the mutated session snapshot, boundary receipt, generated machine
2686    /// lifecycle record, and input/outbox state must become visible in one
2687    /// transaction. Implementations must never compose this from separate
2688    /// `atomic_apply` and `commit_machine_lifecycle` calls.
2689    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    /// Load all input states for a runtime, one row outcome per stored row.
2712    ///
2713    /// A row whose persisted bytes no longer decode under this binary's
2714    /// contract is surfaced as [`InputStateRow::Corrupt`] instead of failing
2715    /// the whole load: one damaged row must not make every other durable
2716    /// input unreadable. The store never drops or rewrites the damaged row;
2717    /// the caller owns the per-row skip/fail policy
2718    /// ([`RuntimeStore::load_input_states_strict`] is the fail-on-any
2719    /// projection).
2720    async fn load_input_states(
2721        &self,
2722        runtime_id: &LogicalRuntimeId,
2723    ) -> Result<Vec<InputStateRow>, RuntimeStoreError>;
2724
2725    /// Strict projection of [`RuntimeStore::load_input_states`]: every row
2726    /// must decode; the first corrupt row fails the whole load with its row
2727    /// identity in the typed error.
2728    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    /// Load a specific boundary receipt.
2747    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    /// Load the latest committed session snapshot for a runtime, if any.
2755    async fn load_session_snapshot(
2756        &self,
2757        runtime_id: &LogicalRuntimeId,
2758    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2759
2760    /// Remove the latest committed session snapshot for a runtime.
2761    ///
2762    /// This is used only as a fail-closed quarantine path when transcript
2763    /// rewrite audit failure makes the runtime snapshot itself invalid recovery
2764    /// authority and the service cannot restore the previous snapshot. An
2765    /// ordinary downstream compatibility-projection failure must retain the
2766    /// already-committed runtime snapshot for retry.
2767    async fn clear_session_snapshot(
2768        &self,
2769        runtime_id: &LogicalRuntimeId,
2770    ) -> Result<(), RuntimeStoreError>;
2771
2772    /// Replace the latest committed session snapshot only if it still matches
2773    /// `expected_current`.
2774    ///
2775    /// Used by fail-closed recovery when a rejected transcript-rewrite snapshot
2776    /// must be restored to its prior audited value. Implementations must compare
2777    /// and write atomically so recovery cannot overwrite newer runtime authority.
2778    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    /// Remove the latest committed session snapshot only if it still matches
2786    /// `expected_current`.
2787    ///
2788    /// This is the conditional variant of the fail-closed quarantine path.
2789    async fn clear_session_snapshot_if_current(
2790        &self,
2791        runtime_id: &LogicalRuntimeId,
2792        expected_current: &[u8],
2793    ) -> Result<bool, RuntimeStoreError>;
2794
2795    /// Report whether the runtime-projection fallback for `runtime_id` is
2796    /// quarantined.
2797    ///
2798    /// This is a durable single-owner fact: when
2799    /// [`clear_session_snapshot_if_current`](Self::clear_session_snapshot_if_current)
2800    /// matches and DELETEs a rejected runtime snapshot, the same atomic boundary
2801    /// records the quarantine marker. A subsequent live snapshot write clears it.
2802    /// Recovery reads this to decide whether a store-only projection may stand in
2803    /// for an absent runtime snapshot. The default is fail-safe (`false`): stores
2804    /// that cannot record the marker durably never claim a snapshot is
2805    /// quarantined.
2806    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    /// Persist a single input state (for durable-before-ack).
2815    async fn persist_input_state(
2816        &self,
2817        runtime_id: &LogicalRuntimeId,
2818        state: &InputStatePersistenceRecord,
2819    ) -> Result<(), RuntimeStoreError>;
2820
2821    /// Atomically persist a batch of machine-authorized input shell updates.
2822    /// Used by per-input terminal outboxes so an N-input batch can never
2823    /// expose a mixed provisional/finalized or finalized/published phase.
2824    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    /// Atomically replace an exact set of input-state rows only when every
2838    /// currently persisted row is byte-identical to its expected
2839    /// [`StoredInputState`] serialization.
2840    ///
2841    /// Expected and replacement batches must contain the same unique keys and
2842    /// at most [`MAX_INPUT_STATE_BATCH_CAS`] rows. If every current row already
2843    /// equals its replacement, implementations return
2844    /// [`InputStateBatchCasOutcome::Swapped`] without rewriting it; this makes
2845    /// a committed store-first transaction retryable after caller
2846    /// cancellation or acknowledgement loss. Missing rows, mixed
2847    /// expected/replacement images, and any other changed durable rows return
2848    /// [`InputStateBatchCasOutcome::Stale`] without writing a replacement.
2849    /// Implementations must hold one lock/transaction across the complete
2850    /// comparison and write set.
2851    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    /// Atomically replace an exact input-state batch while an external
2867    /// authority fence is held across the target write.
2868    ///
2869    /// Implementations must compare the target rows first, then retain both
2870    /// the target transaction and the external authority guard until every
2871    /// replacement is committed. This is the cold-registration recovery seam:
2872    /// a process whose lease expires or is superseded must never overwrite
2873    /// input work recovered by its successor.
2874    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    /// Load a single input state.
2892    async fn load_input_state(
2893        &self,
2894        runtime_id: &LogicalRuntimeId,
2895        input_id: &InputId,
2896    ) -> Result<Option<StoredInputState>, RuntimeStoreError>;
2897
2898    /// Observe one physical machine-lifecycle row without collapsing corrupt
2899    /// or future-version bytes into absence.
2900    ///
2901    /// This is the recovery/reconciliation read surface. Custom stores must
2902    /// implement it explicitly; the default is capability-unavailable rather
2903    /// than inferring a total observation from the older strict decoder.
2904    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    /// Replace exactly one machine-lifecycle row when it is absent or still
2915    /// has the observed raw-content version.
2916    ///
2917    /// Built-in stores atomically compare the raw-content version and publish
2918    /// the machine-authorized replacement. Binding, generation, fence, and
2919    /// run atoms belong to the dead process that wrote the observed row; they
2920    /// are not durable high-waters and may be cleared by an exact-version
2921    /// cold-normalization CAS. The caller retains the prior raw digest for
2922    /// output-only diagnostics. Conflicts are ordinary level-triggered
2923    /// re-observation; unsupported or malformed bytes return
2924    /// [`RuntimeStoreError::MachineLifecycleRepairBlocked`].
2925    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    /// Replace exactly one lifecycle row while an external authority fence is
2938    /// held across the target write.
2939    ///
2940    /// This is the conditional-registration store seam. Built-in stores call
2941    /// `write_fence` inside the row lock/transaction after the exact raw-row
2942    /// comparison and immediately before publication. Custom stores must opt
2943    /// in explicitly; the default is capability-unavailable rather than an
2944    /// unfenced fallback to compare_and_swap_machine_lifecycle.
2945    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    /// Load the last persisted machine lifecycle record bytes, if any.
2959    ///
2960    /// Implementations return only the opaque bytes previously obtained from
2961    /// [`MachineLifecycleCommit::store_record`]. The runtime crate decodes
2962    /// these bytes through `load_runtime_state` or internal recovery helpers;
2963    /// stores must not promote compatibility rows or bare runtime states into
2964    /// lifecycle authority.
2965    async fn load_machine_lifecycle_record(
2966        &self,
2967        runtime_id: &LogicalRuntimeId,
2968    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2969
2970    /// Atomically commit machine-owned lifecycle state changes.
2971    ///
2972    /// Writes runtime state, generated runtime binding facts, and all input
2973    /// state updates in a single atomic operation. `MachineLifecycleCommit` has
2974    /// no public constructor, so this cannot be used by compatibility callers
2975    /// to pick runtime truth.
2976    async fn commit_machine_lifecycle(
2977        &self,
2978        runtime_id: &LogicalRuntimeId,
2979        commit: MachineLifecycleCommit,
2980        input_states: &[InputStatePersistenceRecord],
2981    ) -> Result<(), RuntimeStoreError>;
2982
2983    /// Atomically publish final unregister lifecycle truth and retire the
2984    /// matching ops-lifecycle epoch.
2985    ///
2986    /// The lifecycle record, input-state updates, and ops snapshot deletion
2987    /// MUST commit in one store transaction (or one indivisible in-memory
2988    /// critical section). A terminal lifecycle record with the old ops epoch
2989    /// still present is forbidden: recovery would otherwise resurrect stale
2990    /// operation/cursor authority after unregister. The commit also carries
2991    /// the exact retired ops epoch; implementations MUST atomically retain a
2992    /// durable deletion-wins fence for it, and every later
2993    /// `persist_ops_lifecycle` for that epoch must return
2994    /// [`RuntimeStoreError::OpsLifecycleEpochRetired`] rather than recreate the
2995    /// row. Implementations must also be idempotent so retry after a process
2996    /// crash following commit converges on the same terminal lifecycle with no
2997    /// ops snapshot and the same epoch fence.
2998    ///
2999    /// `Ok(())` means the whole finalization is visible. Every error except
3000    /// [`RuntimeStoreError::UnregisterFinalizationOutcomeUnknown`] MUST mean
3001    /// none of it is visible. A backend with an ambiguous commit
3002    /// acknowledgement must first resolve that ambiguity internally by
3003    /// reading its transaction authority. It may use the typed unknown error
3004    /// only when it cannot prove either the exact final state or the exact
3005    /// pre-transaction state; callers then retry without a durable rollback.
3006    /// The opaque token also proves the generated `DeleteSnapshot` verdict and
3007    /// bundles the exact lifecycle and input rows selected by the machine.
3008    ///
3009    /// The returned future is also a cancellation boundary: after it is
3010    /// dropped, no mutation from that invocation may become visible later.
3011    /// An implementation may leave the prior pair untouched or finish the
3012    /// entire atomic commit before cancellation is observable, but it must not
3013    /// detach a background write that can cross a same-runtime-ID replacement.
3014    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    /// Atomically initialize the ops lifecycle row if it is absent and return
3026    /// the canonical durable snapshot.
3027    ///
3028    /// The absence check, optional insert, and canonical read MUST share one
3029    /// store transaction (or one indivisible in-memory critical section).
3030    /// Concurrent initializer calls for the same runtime must therefore all
3031    /// observe the same epoch: exactly one candidate may become durable and
3032    /// every losing caller receives that winner's snapshot. The machine's
3033    /// stable registration transaction separately spans this store call
3034    /// through map publication/removal; this method is not a distributed
3035    /// machine lease. Implementations must also reject a candidate whose epoch
3036    /// is already covered by the unregister deletion-wins fence.
3037    ///
3038    /// Cancellation may leave the candidate as the canonical empty row: no
3039    /// bindings escape before this await completes, and the next registrar
3040    /// adopts the returned durable epoch. A cancelled invocation must never
3041    /// overwrite a row that was already present.
3042    ///
3043    /// There is intentionally no load-then-persist default. Custom stores
3044    /// that support durable ops lifecycle state must implement this atomic
3045    /// boundary or fail closed with [`RuntimeStoreError::Unsupported`].
3046    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    /// Persist a snapshot of the ops lifecycle registry state.
3058    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    /// Load a previously persisted ops lifecycle snapshot.
3070    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    /// Delete a previously persisted ops lifecycle snapshot.
3079    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    // -----------------------------------------------------------------------
3090    // Mob host binding rows (`runtime_mob_host_bindings`, multi-host mobs R8)
3091    // -----------------------------------------------------------------------
3092    //
3093    // Raw record-JSON accessors only: the TYPED record and the
3094    // transition-derived persistence authorities live mob-side
3095    // (`meerkat-mob/src/runtime/host_actor.rs`); this store never interprets
3096    // the blob. CAS compares the full serialized record, mirroring the
3097    // `mob_runtime_supervisors` mechanics.
3098
3099    /// Load the persisted host-binding record blob for `mob_id`, if any.
3100    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    /// List every persisted host-binding row (boot recovery).
3111    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    /// Insert the host-binding row for `mob_id` iff absent. Returns whether
3118    /// the row was inserted.
3119    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    /// Replace the host-binding row for `mob_id` iff the stored blob equals
3131    /// `expected_json`. Returns whether the swap applied.
3132    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    /// Delete the host-binding row for `mob_id` iff the stored blob equals
3145    /// `expected_json`. Returns whether a row was deleted.
3146    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    /// Load the durable receipt for an already-completed host revocation.
3158    ///
3159    /// The blob is deliberately separate from `runtime_mob_host_bindings`:
3160    /// boot recovery must never mistake a revoke retry receipt for a live
3161    /// binding or revive the materialized-member rows that the revoke
3162    /// removed. The typed receipt and its transition witness live mob-side;
3163    /// this store treats it as opaque bytes.
3164    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    /// List durable host-revocation receipts for boot recovery of exact
3175    /// reply-loss retries. Receipts are not bindings and carry no member
3176    /// revival rows.
3177    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    /// Atomically delete the expected active binding and publish its revoke
3184    /// receipt. Returns `false` when the expected binding did not match; in
3185    /// that case neither write is visible.
3186    ///
3187    /// This is the durable terminal boundary for host revocation. A crash
3188    /// before it leaves the binding retryable; a crash after it leaves no
3189    /// binding/member rows to revive and an exact receipt to replay.
3190    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}