1use std::fmt;
32
33use serde::de::{self, Deserializer, Visitor};
34use serde::{Deserialize, Serialize, Serializer};
35
36use super::config::ResolvedOperationConfig;
37use super::effect::{Digest, KernelEffect, LaunchToken, wire_opaque_ref};
38use super::envelope::OperationLifecycle;
39use super::fault::{KernelFault, KernelFaultCode};
40use super::record::{NormalizedInput, RecordError, canonical_bytes, canonical_digest};
41use super::root::{ExecutionFocus, LogicalAgentSpec, LogicalTask, RootKind};
42use super::scalar::{
43 AttemptId, BoundedJson, CanonicalBytes, EffectId, InputId, MemoryBindingId, NodeId,
44 OperationId, SCALAR_ERROR_MARKER, SignalId, TaskId, WireScalarError, WireU64, WorkflowId,
45};
46use super::syscall::MemoryKind;
47use super::terminal::KernelTerminal;
48
49pub const CHECKPOINT_ERROR_MARKER: &str = "kernel checkpoint rejected";
55
56#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CheckpointError {
64 Incompatible(String),
67 Corrupted(String),
70 NotCanonical(String),
72}
73
74impl CheckpointError {
75 pub fn message(&self) -> &str {
76 match self {
77 Self::Incompatible(message)
78 | Self::Corrupted(message)
79 | Self::NotCanonical(message) => message,
80 }
81 }
82
83 pub fn code(&self) -> KernelFaultCode {
84 match self {
85 Self::Incompatible(_) => KernelFaultCode::CheckpointIncompatible,
86 Self::Corrupted(_) => KernelFaultCode::CheckpointCorrupted,
87 Self::NotCanonical(_) => KernelFaultCode::MalformedEnvelope,
88 }
89 }
90
91 pub fn fault(&self) -> KernelFault {
93 KernelFault::new(self.code(), self.to_string())
94 }
95}
96
97impl fmt::Display for CheckpointError {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(
106 f,
107 "{CHECKPOINT_ERROR_MARKER} ({}): {}",
108 self.code().as_str(),
109 self.message()
110 )
111 }
112}
113
114impl std::error::Error for CheckpointError {}
115
116impl From<RecordError> for CheckpointError {
117 fn from(error: RecordError) -> Self {
118 Self::NotCanonical(error.message().to_string())
119 }
120}
121
122wire_opaque_ref!(
123 CheckpointAckToken,
130 "checkpoint ack token"
131);
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct CanonicalInput {
146 pub step_seq: WireU64,
147 pub record_digest: Digest,
148 pub input: NormalizedInput,
149}
150
151impl CanonicalInput {
152 pub fn from_record(record: &super::record::KernelRecord) -> Result<Self, CheckpointError> {
154 Ok(Self {
155 step_seq: record.step_seq(),
156 record_digest: record.record_digest().clone(),
157 input: record.normalized_input()?,
158 })
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174#[serde(deny_unknown_fields)]
175pub struct LogicalKernelState {
176 pub transition: TransitionState,
177 pub syscall: SyscallState,
178 pub scheduler: SchedulerState,
179 pub context_vm: ContextVmState,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct TransitionState {
191 pub lifecycle: OperationLifecycle,
192 pub resolved_config: ResolvedOperationConfig,
199 #[serde(default)]
201 pub root_kind: Option<RootKind>,
202 #[serde(default)]
205 pub focus: Option<ExecutionFocus>,
206 pub last_observed_at_ms: WireU64,
209 #[serde(default)]
212 pub pending_effects: Vec<KernelEffect>,
213 #[serde(default)]
216 pub resolved_effects: Vec<ResolvedEffectState>,
217 #[serde(default)]
220 pub launch_tokens: Vec<LaunchTokenState>,
221 #[serde(default)]
224 pub accepted_inputs: Vec<AcceptedInputState>,
225 #[serde(default)]
228 pub accepted_cancellation: Option<AcceptedCancellationState>,
229 #[serde(default)]
231 pub terminal: Option<KernelTerminal>,
232}
233
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(deny_unknown_fields)]
236pub struct ResolvedEffectState {
237 pub effect_id: EffectId,
238 pub outcome_digest: Digest,
239 pub input_id: InputId,
240 pub step_seq: WireU64,
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245pub struct LaunchTokenState {
246 pub launch_token: LaunchToken,
247 pub step_seq: WireU64,
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259pub struct AcceptedInputState {
260 pub input_id: InputId,
261 pub step_seq: WireU64,
262 pub record_digest: Digest,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(deny_unknown_fields)]
267pub struct AcceptedCancellationState {
268 pub command_digest: Digest,
270 pub input_id: InputId,
271 pub step_seq: WireU64,
272}
273
274#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
277#[serde(deny_unknown_fields)]
278pub struct SyscallState {
279 #[serde(default)]
282 pub policy_revision: Option<WireU64>,
283 #[serde(default)]
286 pub live_config: Option<ResolvedOperationConfig>,
287 #[serde(default)]
290 pub provider_calls: Vec<PendingProviderCallState>,
291 #[serde(default)]
293 pub consumed_call_ids: Vec<String>,
294 #[serde(default)]
297 pub authored_memory_writes: Vec<AuthoredMemoryWriteState>,
298 #[serde(default)]
299 pub authored_memory_queries: Vec<AuthoredMemoryQueryState>,
300 #[serde(default)]
303 pub memory_write_window_ms: Vec<WireU64>,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct PendingProviderCallState {
309 pub effect_id: EffectId,
310 pub task_id: TaskId,
312 pub exposed_tools: Vec<String>,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct AuthoredMemoryWriteState {
318 pub effect_id: EffectId,
319 pub binding_id: MemoryBindingId,
320 pub name: String,
321 pub kind: MemoryKind,
322 pub size_bytes: u32,
323}
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326#[serde(deny_unknown_fields)]
327pub struct AuthoredMemoryQueryState {
328 pub effect_id: EffectId,
329 pub binding_id: MemoryBindingId,
330 pub text: String,
331 pub requested_k: u32,
332}
333
334#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
337#[serde(deny_unknown_fields)]
338pub struct SchedulerState {
339 pub run_spec: Option<LogicalAgentSpec>,
342 pub advertised_tool_ids: Option<Vec<String>>,
345 pub turn: u32,
346 pub total_tokens: WireU64,
347 pub rounds_completed: u32,
348 pub subagents_spawned: u32,
349 #[serde(default)]
351 pub started_at_ms: Option<WireU64>,
352 #[serde(default)]
356 pub wall_budget_ms: Option<WireU64>,
357 #[serde(default)]
360 pub tasks: Vec<TaskControlState>,
361 #[serde(default)]
365 pub attempts: Vec<TaskAttemptState>,
366 #[serde(default)]
367 pub workflow: Option<WorkflowGraphState>,
368 #[serde(default)]
369 pub queued_signals: Vec<QueuedSignalState>,
370 #[serde(default)]
372 pub signal_dedupe_keys: Vec<String>,
373 #[serde(default)]
374 pub milestone: Option<MilestoneState>,
375 #[serde(default)]
379 pub entropy: EntropyState,
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
382 pub channels: Vec<LocalChannelState>,
383 #[serde(default, skip_serializing_if = "Vec::is_empty")]
385 pub objects: Vec<crate::mm::handle::ObjectDescriptor>,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[serde(deny_unknown_fields)]
390pub struct LocalChannelState {
391 pub channel_id: String,
392 pub channel: crate::scheduler::mailbox::Channel,
393}
394
395#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(deny_unknown_fields)]
397pub struct EntropyState {
398 #[serde(default)]
400 pub window: Vec<EntropyTurnState>,
401 pub rollbacks_pending: u32,
403 pub disarmed: bool,
405 #[serde(default)]
407 pub last_alert_turn: Option<u32>,
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
411#[serde(deny_unknown_fields)]
412pub struct EntropyTurnState {
413 pub errored_results: u32,
414 pub total_results: u32,
415 pub rollbacks: u32,
416}
417
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
427#[serde(deny_unknown_fields)]
428pub struct TaskControlState {
429 pub task_id: TaskId,
430 #[serde(default)]
431 pub parent_task_id: Option<TaskId>,
432 pub lifecycle: String,
433 #[serde(default, skip_serializing_if = "is_nested_runnable_cause")]
434 pub runnable_cause: crate::scheduler::tcb::RunnableCause,
435 #[serde(default)]
437 pub termination: Option<String>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub wait_set: Option<TaskWaitSetState>,
441 #[serde(default)]
442 pub capability_ids: Vec<String>,
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
446 pub capabilities: Vec<crate::types::capability::Capability>,
447 #[serde(default)]
449 pub process: Option<ChildProcessState>,
450 #[serde(default, skip_serializing_if = "is_default_supervision")]
451 pub supervision: crate::scheduler::tcb::SupervisionPolicy,
452 #[serde(default, skip_serializing_if = "Vec::is_empty")]
453 pub supervision_events: Vec<crate::scheduler::tcb::SupervisionEvent>,
454 pub tokens_used: WireU64,
455 pub turns_used: u32,
456 #[serde(default)]
461 pub child_budget_remaining: Option<crate::scheduler::budget_grant::ResourceBudget>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub budget_grant: Option<crate::scheduler::budget_grant::BudgetGrant>,
466 #[serde(
468 default,
469 skip_serializing_if = "crate::scheduler::mailbox::Mailbox::is_empty"
470 )]
471 pub mailbox: crate::scheduler::mailbox::Mailbox,
472}
473
474fn is_default_supervision(value: &crate::scheduler::tcb::SupervisionPolicy) -> bool {
475 value == &crate::scheduler::tcb::SupervisionPolicy::default()
476}
477
478fn is_nested_runnable_cause(value: &crate::scheduler::tcb::RunnableCause) -> bool {
479 *value == crate::scheduler::tcb::RunnableCause::NestedTask
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(deny_unknown_fields)]
484pub struct TaskWaitSetState {
485 pub mode: String,
486 pub conditions: Vec<TaskWaitConditionState>,
487 #[serde(default, skip_serializing_if = "Vec::is_empty")]
488 pub satisfied: Vec<u32>,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(tag = "kind", rename_all = "snake_case")]
493pub enum TaskWaitConditionState {
494 Effect { effect_id: EffectId },
495 Child { task_id: TaskId },
496 Children { task_ids: Vec<TaskId> },
497 Approval { approval_id: String },
498 Signal { filter: String },
499 Timer { deadline_ms: WireU64 },
500 Channel { channel_id: String },
501 Resource { resource_key: String },
502 External { subscription_id: String },
503}
504
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct ChildProcessState {
508 pub role: String,
509 pub isolation: String,
510 pub context_inheritance: String,
511 #[serde(default)]
513 pub join_result: Option<BoundedJson>,
514}
515
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517#[serde(deny_unknown_fields)]
518pub struct TaskAttemptState {
519 pub task_id: TaskId,
520 pub attempt_id: AttemptId,
521}
522
523#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
524#[serde(deny_unknown_fields)]
525pub struct WorkflowGraphState {
526 pub workflow_id: WorkflowId,
527 #[serde(default)]
530 pub nodes: Vec<WorkflowNodeState>,
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[serde(deny_unknown_fields)]
539pub struct WorkflowNodeState {
540 pub node_id: NodeId,
541 pub task: LogicalTask,
542 #[serde(default)]
543 pub depends_on: Vec<NodeId>,
544 #[serde(default)]
545 pub run_spec: Option<LogicalAgentSpec>,
546 pub kind: String,
547 pub status: String,
548 #[serde(default)]
550 pub active_agent_id: Option<String>,
551 #[serde(default)]
552 pub iterations_completed: u32,
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
556#[serde(deny_unknown_fields)]
557pub struct QueuedSignalState {
558 pub signal_id: SignalId,
559 pub source: String,
560 pub signal_type: String,
561 pub urgency: String,
562 pub summary: String,
563 #[serde(default)]
564 pub payload: BoundedJson,
565 #[serde(default)]
566 pub dedupe_key: Option<String>,
567 #[serde(default)]
568 pub deadline_ms: Option<WireU64>,
569 #[serde(default)]
570 pub coalesce_key: Option<String>,
571 pub coalesced_count: u32,
572 #[serde(default)]
573 pub recipient: Option<String>,
574 pub timestamp_ms: WireU64,
575 pub deadline_escalated: bool,
576 #[serde(default)]
578 pub dedupe_keys: Vec<String>,
579}
580
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582#[serde(deny_unknown_fields)]
583pub struct MilestoneState {
584 pub contract_id: String,
587 #[serde(default)]
588 pub phase_id: Option<String>,
589 pub complete: bool,
590 #[serde(default)]
593 pub blocked_count: u32,
594}
595
596#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
613#[serde(deny_unknown_fields)]
614pub struct ContextVmState {
615 #[serde(default)]
617 pub state_generation: u64,
618 #[serde(default)]
620 pub system_measurements: Vec<crate::context::measurement::TokenMeasurement>,
621 #[serde(default)]
622 pub history_measurements: Vec<crate::context::measurement::TokenMeasurement>,
623 #[serde(default)]
624 pub knowledge_reference_step: u64,
625 #[serde(default)]
626 pub knowledge_budget_warned: bool,
627 #[serde(default)]
629 pub handles: Vec<HandleState>,
630 pub next_handle_id: u32,
633 #[serde(default)]
637 pub pending_payload_loads: Vec<PendingPayloadLoadState>,
638 #[serde(default)]
639 pub active_skills: Vec<SkillLeaseState>,
640 #[serde(default)]
641 pub knowledge: Vec<KnowledgeSlotState>,
642 #[serde(default)]
643 pub signals: Vec<String>,
644 #[serde(default)]
649 pub messages: Vec<StoredMessageState>,
650 pub task_state: LogicalTaskState,
654 pub partition_tokens: PartitionTokenState,
655 pub history_len: u32,
656 #[serde(default)]
658 pub frozen_history_len: u32,
659 pub last_activity_ms: WireU64,
660 #[serde(default)]
661 pub last_compact_ms: Option<WireU64>,
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
669#[serde(rename_all = "snake_case")]
670pub enum MessagePartition {
671 System,
672 History,
673}
674
675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
686#[serde(deny_unknown_fields)]
687pub struct StoredMessageState {
688 pub partition: MessagePartition,
689 pub role: String,
691 pub body: StoredMessageBody,
692 #[serde(default)]
695 pub tool_calls: Vec<LogicalToolCall>,
696 pub tokens: u32,
699}
700
701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
708#[serde(tag = "form", rename_all = "snake_case")]
709pub enum StoredMessageBody {
710 Inline(InlineMessageBody),
712 Reference(ReferencedMessageBody),
715 Structured(StructuredMessageBody),
721}
722
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724#[serde(deny_unknown_fields)]
725pub struct InlineMessageBody {
726 pub text: String,
727 #[serde(default)]
729 pub tool_call_id: Option<String>,
730 #[serde(default)]
731 pub is_error: bool,
732}
733
734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735#[serde(deny_unknown_fields)]
736pub struct ReferencedMessageBody {
737 pub handle_id: u32,
740 pub digest: String,
742 pub preview: String,
744 #[serde(default)]
745 pub tool_call_id: Option<String>,
746 #[serde(default)]
747 pub is_error: bool,
748}
749
750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
751#[serde(deny_unknown_fields)]
752pub struct StructuredMessageBody {
753 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub durable_content: Option<crate::types::durable_content::DurableContent>,
756 #[serde(default, skip_serializing_if = "Vec::is_empty")]
759 pub durable_tool_results: Vec<crate::types::durable_content::DurableToolResult>,
760}
761
762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
763#[serde(deny_unknown_fields)]
764pub struct LogicalToolCall {
765 pub call_id: String,
766 pub name: String,
767 pub arguments: String,
770}
771
772#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
778#[serde(deny_unknown_fields)]
779pub struct LogicalTaskState {
780 #[serde(default)]
781 pub goal: String,
782 #[serde(default)]
783 pub criteria: Vec<String>,
784 #[serde(default)]
785 pub plan: Vec<LogicalPlanStep>,
786 #[serde(default)]
787 pub current_step: Option<u32>,
788 #[serde(default)]
789 pub progress: String,
790 #[serde(default)]
791 pub scratchpad: String,
792 #[serde(default)]
793 pub blocked_on: Vec<String>,
794 #[serde(default)]
795 pub directives: Vec<String>,
796 #[serde(default)]
797 pub preserved_refs: Vec<String>,
798 #[serde(default)]
799 pub recent_actions: Vec<String>,
800 #[serde(default)]
801 pub compression_log: Vec<LogicalCompressionEntry>,
802 #[serde(default)]
803 pub compression_log_dropped: WireU64,
804}
805
806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
807#[serde(deny_unknown_fields)]
808pub struct LogicalPlanStep {
809 pub label: String,
810 pub done: bool,
811}
812
813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct LogicalCompressionEntry {
816 pub action: String,
817 pub summary: String,
818}
819
820#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823#[serde(deny_unknown_fields)]
824pub struct HandleState {
825 pub handle_id: u32,
826 pub kind: String,
827 pub residency: String,
828 #[serde(default)]
829 pub payload_ref: Option<String>,
830 #[serde(default)]
831 pub digest: Option<String>,
832 #[serde(default)]
833 pub original_size: Option<WireU64>,
834 pub tokens: u32,
835 #[serde(default)]
837 pub source: Option<String>,
838}
839
840#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
841#[serde(deny_unknown_fields)]
842pub struct PendingPayloadLoadState {
843 pub effect_id: EffectId,
844 pub handle_id: String,
845 pub digest: String,
846 #[serde(default)]
847 pub original_size: Option<WireU64>,
848}
849
850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
851#[serde(deny_unknown_fields)]
852pub struct SkillLeaseState {
853 pub skill: String,
854 #[serde(default)]
856 pub lease_until_turn: Option<u32>,
857}
858
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
866#[serde(deny_unknown_fields)]
867pub struct KnowledgeSlotState {
868 #[serde(default)]
870 pub key: Option<String>,
871 pub role: String,
872 pub body: StoredMessageBody,
873 pub tokens: u32,
874 pub pinned: bool,
875 pub evict_at_boundary: bool,
876 #[serde(default)]
877 pub tool_calls: Vec<LogicalToolCall>,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub pending: Option<Box<StoredMessageState>>,
881 #[serde(default)]
882 pub use_count: u64,
883 #[serde(default)]
884 pub last_used_step: Option<u64>,
885}
886
887#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(deny_unknown_fields)]
889pub struct PartitionTokenState {
890 pub system: u32,
891 pub knowledge: u32,
892 pub history: u32,
893}
894
895#[derive(Debug, Clone, PartialEq)]
901pub struct LogicalStateProjection {
902 pub root_kind: Option<RootKind>,
903 pub focus: Option<ExecutionFocus>,
904 pub syscall: SyscallState,
905 pub scheduler: SchedulerState,
906 pub context_vm: ContextVmState,
907}
908
909#[derive(Debug, Clone, PartialEq)]
916pub struct CheckpointDraft {
917 pub operation_id: OperationId,
918 pub genesis_digest: Digest,
919 pub base_step_seq: WireU64,
920 pub base_record_digest: Digest,
921 pub through_step_seq: WireU64,
922 pub covered_transaction_head_digest: Digest,
923 pub logical_state: LogicalKernelState,
924 pub tail_inputs: Vec<CanonicalInput>,
925}
926
927#[derive(Debug, Clone, PartialEq, Serialize)]
935pub struct KernelCheckpoint {
936 operation_id: OperationId,
937 genesis_digest: Digest,
940 base_step_seq: WireU64,
942 base_record_digest: Digest,
949 through_step_seq: WireU64,
951 covered_transaction_head_digest: Digest,
954 logical_state: LogicalKernelState,
955 tail_inputs: Vec<CanonicalInput>,
956 state_digest: Digest,
957 tail_digest: Digest,
958 checkpoint_digest: Digest,
959}
960
961#[derive(Serialize)]
963struct CheckpointBody<'a> {
964 operation_id: &'a OperationId,
965 genesis_digest: &'a Digest,
966 base_step_seq: WireU64,
967 base_record_digest: &'a Digest,
968 through_step_seq: WireU64,
969 covered_transaction_head_digest: &'a Digest,
970 logical_state: &'a LogicalKernelState,
971 tail_inputs: &'a [CanonicalInput],
972 state_digest: &'a Digest,
973 tail_digest: &'a Digest,
974}
975
976impl KernelCheckpoint {
977 pub fn assemble(draft: CheckpointDraft) -> Result<Self, CheckpointError> {
980 let CheckpointDraft {
981 operation_id,
982 genesis_digest,
983 base_step_seq,
984 base_record_digest,
985 through_step_seq,
986 covered_transaction_head_digest,
987 logical_state,
988 tail_inputs,
989 } = draft;
990
991 validate_durable_message_bodies(&logical_state.context_vm)?;
992
993 check_tail(
994 &operation_id,
995 base_step_seq,
996 &base_record_digest,
997 through_step_seq,
998 &covered_transaction_head_digest,
999 &tail_inputs,
1000 )?;
1001
1002 let state_digest = canonical_digest(canonical_bytes(&logical_state)?.as_slice());
1003 let tail_digest = canonical_digest(canonical_bytes(&tail_inputs)?.as_slice());
1004 let checkpoint_digest = Self::body_digest(&CheckpointBody {
1005 operation_id: &operation_id,
1006 genesis_digest: &genesis_digest,
1007 base_step_seq,
1008 base_record_digest: &base_record_digest,
1009 through_step_seq,
1010 covered_transaction_head_digest: &covered_transaction_head_digest,
1011 logical_state: &logical_state,
1012 tail_inputs: &tail_inputs,
1013 state_digest: &state_digest,
1014 tail_digest: &tail_digest,
1015 })?;
1016
1017 Ok(Self {
1018 operation_id,
1019 genesis_digest,
1020 base_step_seq,
1021 base_record_digest,
1022 through_step_seq,
1023 covered_transaction_head_digest,
1024 logical_state,
1025 tail_inputs,
1026 state_digest,
1027 tail_digest,
1028 checkpoint_digest,
1029 })
1030 }
1031
1032 fn body_digest(body: &CheckpointBody<'_>) -> Result<Digest, CheckpointError> {
1033 Ok(canonical_digest(canonical_bytes(body)?.as_slice()))
1034 }
1035
1036 pub fn operation_id(&self) -> &OperationId {
1039 &self.operation_id
1040 }
1041
1042 pub fn genesis_digest(&self) -> &Digest {
1043 &self.genesis_digest
1044 }
1045
1046 pub fn base_step_seq(&self) -> WireU64 {
1047 self.base_step_seq
1048 }
1049
1050 pub fn base_record_digest(&self) -> &Digest {
1051 &self.base_record_digest
1052 }
1053
1054 pub fn through_step_seq(&self) -> WireU64 {
1055 self.through_step_seq
1056 }
1057
1058 pub fn covered_transaction_head_digest(&self) -> &Digest {
1059 &self.covered_transaction_head_digest
1060 }
1061
1062 pub fn logical_state(&self) -> &LogicalKernelState {
1063 &self.logical_state
1064 }
1065
1066 pub fn tail_inputs(&self) -> &[CanonicalInput] {
1067 &self.tail_inputs
1068 }
1069
1070 pub fn state_digest(&self) -> &Digest {
1071 &self.state_digest
1072 }
1073
1074 pub fn tail_digest(&self) -> &Digest {
1075 &self.tail_digest
1076 }
1077
1078 pub fn checkpoint_digest(&self) -> &Digest {
1079 &self.checkpoint_digest
1080 }
1081
1082 pub fn checkpoint_bytes(&self) -> CanonicalBytes {
1086 canonical_bytes(self).expect("a checkpoint contains only canonical scalars")
1087 }
1088
1089 pub fn from_checkpoint_bytes(bytes: &[u8]) -> Result<Self, CheckpointError> {
1091 let text = std::str::from_utf8(bytes).map_err(|error| {
1092 CheckpointError::NotCanonical(format!("checkpoint bytes are not UTF-8: {error}"))
1093 })?;
1094 let document =
1095 serde_json::from_str(text).map_err(|error| decode_error(&error.to_string()))?;
1096 decode_checkpoint_value(document)
1097 }
1098
1099 pub fn boundary(&self) -> super::transaction::CheckpointBoundary {
1101 super::transaction::CheckpointBoundary {
1102 through_step_seq: self.through_step_seq,
1103 covered_head: self.covered_transaction_head_digest.clone(),
1104 }
1105 }
1106
1107 pub fn into_candidate(self) -> CheckpointCandidate {
1109 let ack_token = ack_token_for(
1110 &self.operation_id,
1111 self.through_step_seq,
1112 &self.checkpoint_digest,
1113 );
1114 CheckpointCandidate {
1115 checkpoint_bytes: self.checkpoint_bytes(),
1116 through_step_seq: self.through_step_seq,
1117 covered_head: self.covered_transaction_head_digest.clone(),
1118 state_digest: self.state_digest.clone(),
1119 ack_token,
1120 }
1121 }
1122
1123 pub fn verify(&self) -> Result<(), CheckpointError> {
1130 validate_durable_message_bodies(&self.logical_state.context_vm)?;
1131 check_tail(
1132 &self.operation_id,
1133 self.base_step_seq,
1134 &self.base_record_digest,
1135 self.through_step_seq,
1136 &self.covered_transaction_head_digest,
1137 &self.tail_inputs,
1138 )?;
1139
1140 let state_digest = canonical_digest(canonical_bytes(&self.logical_state)?.as_slice());
1141 if state_digest != self.state_digest {
1142 return Err(CheckpointError::Corrupted(format!(
1143 "checkpoint {} through step {}: the logical state hashes to {state_digest}, \
1144 but the checkpoint claims {}",
1145 self.operation_id, self.through_step_seq, self.state_digest
1146 )));
1147 }
1148 let tail_digest = canonical_digest(canonical_bytes(&self.tail_inputs)?.as_slice());
1149 if tail_digest != self.tail_digest {
1150 return Err(CheckpointError::Corrupted(format!(
1151 "checkpoint {} through step {}: the bounded tail hashes to {tail_digest}, \
1152 but the checkpoint claims {}",
1153 self.operation_id, self.through_step_seq, self.tail_digest
1154 )));
1155 }
1156 let checkpoint_digest = Self::body_digest(&CheckpointBody {
1157 operation_id: &self.operation_id,
1158 genesis_digest: &self.genesis_digest,
1159 base_step_seq: self.base_step_seq,
1160 base_record_digest: &self.base_record_digest,
1161 through_step_seq: self.through_step_seq,
1162 covered_transaction_head_digest: &self.covered_transaction_head_digest,
1163 logical_state: &self.logical_state,
1164 tail_inputs: &self.tail_inputs,
1165 state_digest: &self.state_digest,
1166 tail_digest: &self.tail_digest,
1167 })?;
1168 if checkpoint_digest != self.checkpoint_digest {
1169 return Err(CheckpointError::Corrupted(format!(
1170 "checkpoint {} through step {}: the body hashes to {checkpoint_digest}, \
1171 but the checkpoint claims {}",
1172 self.operation_id, self.through_step_seq, self.checkpoint_digest
1173 )));
1174 }
1175 Ok(())
1176 }
1177
1178 pub fn verify_belongs_to(
1180 &self,
1181 operation_id: &OperationId,
1182 genesis_digest: &Digest,
1183 ) -> Result<(), CheckpointError> {
1184 if &self.operation_id != operation_id {
1185 return Err(CheckpointError::Incompatible(format!(
1186 "checkpoint belongs to operation {}, this runtime to {operation_id}",
1187 self.operation_id
1188 )));
1189 }
1190 if &self.genesis_digest != genesis_digest {
1191 return Err(CheckpointError::Incompatible(format!(
1192 "checkpoint {operation_id} binds genesis {}, this journal's genesis is \
1193 {genesis_digest}",
1194 self.genesis_digest
1195 )));
1196 }
1197 Ok(())
1198 }
1199}
1200
1201fn validate_durable_message_bodies(context: &ContextVmState) -> Result<(), CheckpointError> {
1202 let bodies = context
1203 .messages
1204 .iter()
1205 .map(|message| &message.body)
1206 .chain(context.knowledge.iter().map(|slot| &slot.body))
1207 .chain(
1208 context
1209 .knowledge
1210 .iter()
1211 .filter_map(|slot| slot.pending.as_ref().map(|pending| &pending.body)),
1212 );
1213 for body in bodies {
1214 let StoredMessageBody::Structured(structured) = body else {
1215 continue;
1216 };
1217 let body_forms = usize::from(structured.durable_content.is_some())
1218 + usize::from(!structured.durable_tool_results.is_empty());
1219 if body_forms > 1 {
1220 return Err(CheckpointError::Incompatible(
1221 "structured message carries more than one durable body form".into(),
1222 ));
1223 }
1224 if !structured.durable_tool_results.is_empty() {
1225 for result in &structured.durable_tool_results {
1226 result.validate().map_err(|error| {
1227 CheckpointError::Incompatible(format!(
1228 "structured message carries invalid durable tool result: {error}"
1229 ))
1230 })?;
1231 }
1232 } else if let Some(content) = &structured.durable_content {
1233 content.validate().map_err(|error| {
1234 CheckpointError::Incompatible(format!(
1235 "structured message carries invalid durable content: {error}"
1236 ))
1237 })?;
1238 } else {
1239 return Err(CheckpointError::Incompatible(
1240 "structured message carries no durable content".into(),
1241 ));
1242 }
1243 }
1244 Ok(())
1245}
1246
1247fn check_tail(
1254 operation_id: &OperationId,
1255 base_step_seq: WireU64,
1256 base_record_digest: &Digest,
1257 through_step_seq: WireU64,
1258 covered_transaction_head_digest: &Digest,
1259 tail_inputs: &[CanonicalInput],
1260) -> Result<(), CheckpointError> {
1261 if base_step_seq > through_step_seq {
1262 return Err(CheckpointError::Corrupted(format!(
1263 "checkpoint {operation_id} bases at step {base_step_seq} but covers only through \
1264 {through_step_seq}"
1265 )));
1266 }
1267 if base_step_seq == through_step_seq && base_record_digest != covered_transaction_head_digest {
1271 return Err(CheckpointError::Corrupted(format!(
1272 "checkpoint {operation_id} covers no tail, so its base {base_record_digest} and its \
1273 covered head {covered_transaction_head_digest} name the same record — but they differ"
1274 )));
1275 }
1276 let expected = through_step_seq.get() - base_step_seq.get();
1277 if tail_inputs.len() as u64 != expected {
1278 return Err(CheckpointError::Corrupted(format!(
1279 "checkpoint {operation_id} covers ({base_step_seq}, {through_step_seq}] — {expected} \
1280 inputs — but its bounded tail holds {}",
1281 tail_inputs.len()
1282 )));
1283 }
1284 for (offset, entry) in tail_inputs.iter().enumerate() {
1285 let want = base_step_seq.get() + offset as u64 + 1;
1286 if entry.step_seq.get() != want {
1287 return Err(CheckpointError::Corrupted(format!(
1288 "checkpoint {operation_id} bounded tail is not the contiguous range \
1289 ({base_step_seq}, {through_step_seq}]: position {offset} is step {} where step \
1290 {want} was due",
1291 entry.step_seq
1292 )));
1293 }
1294 if &entry.input.operation_id != operation_id {
1295 return Err(CheckpointError::Incompatible(format!(
1296 "checkpoint {operation_id} bounded tail carries an input of operation {} at step \
1297 {}",
1298 entry.input.operation_id, entry.step_seq
1299 )));
1300 }
1301 }
1302 if let Some(last) = tail_inputs.last()
1305 && &last.record_digest != covered_transaction_head_digest
1306 {
1307 return Err(CheckpointError::Corrupted(format!(
1308 "checkpoint {operation_id} claims covered head {covered_transaction_head_digest}, but \
1309 its bounded tail ends at {} on step {}",
1310 last.record_digest, last.step_seq
1311 )));
1312 }
1313 Ok(())
1314}
1315
1316fn ack_token_for(
1317 operation_id: &OperationId,
1318 through_step_seq: WireU64,
1319 checkpoint_digest: &Digest,
1320) -> CheckpointAckToken {
1321 CheckpointAckToken::new(format!(
1322 "{operation_id}:checkpoint:{through_step_seq}:{checkpoint_digest}"
1323 ))
1324 .expect("an operation-scoped checkpoint ack token is always a legal branded ref")
1325}
1326
1327#[derive(Debug, Clone, PartialEq)]
1334pub struct CheckpointCandidate {
1335 pub checkpoint_bytes: CanonicalBytes,
1336 pub through_step_seq: WireU64,
1337 pub covered_head: Digest,
1338 pub state_digest: Digest,
1339 pub ack_token: CheckpointAckToken,
1340}
1341
1342impl CheckpointCandidate {
1343 pub fn boundary(&self) -> super::transaction::CheckpointBoundary {
1345 super::transaction::CheckpointBoundary {
1346 through_step_seq: self.through_step_seq,
1347 covered_head: self.covered_head.clone(),
1348 }
1349 }
1350
1351 pub fn decode(&self) -> Result<KernelCheckpoint, CheckpointError> {
1354 KernelCheckpoint::from_checkpoint_bytes(self.checkpoint_bytes.as_slice())
1355 }
1356}
1357
1358#[derive(Deserialize)]
1366#[serde(deny_unknown_fields)]
1367struct CheckpointProjection {
1368 operation_id: OperationId,
1369 genesis_digest: Digest,
1370 base_step_seq: WireU64,
1371 base_record_digest: Digest,
1372 through_step_seq: WireU64,
1373 covered_transaction_head_digest: Digest,
1374 logical_state: LogicalKernelState,
1375 tail_inputs: Vec<CanonicalInput>,
1376 state_digest: Digest,
1377 tail_digest: Digest,
1378 checkpoint_digest: Digest,
1379}
1380
1381fn decode_checkpoint_value(
1382 document: serde_json::Value,
1383) -> Result<KernelCheckpoint, CheckpointError> {
1384 decode_current_checkpoint(document)
1385}
1386
1387fn decode_current_checkpoint(
1388 document: serde_json::Value,
1389) -> Result<KernelCheckpoint, CheckpointError> {
1390 let projection = serde_json::from_value::<CheckpointProjection>(document)
1391 .map_err(|error| decode_error(&error.to_string()))?;
1392 let checkpoint = KernelCheckpoint {
1393 operation_id: projection.operation_id,
1394 genesis_digest: projection.genesis_digest,
1395 base_step_seq: projection.base_step_seq,
1396 base_record_digest: projection.base_record_digest,
1397 through_step_seq: projection.through_step_seq,
1398 covered_transaction_head_digest: projection.covered_transaction_head_digest,
1399 logical_state: projection.logical_state,
1400 tail_inputs: projection.tail_inputs,
1401 state_digest: projection.state_digest,
1402 tail_digest: projection.tail_digest,
1403 checkpoint_digest: projection.checkpoint_digest,
1404 };
1405 checkpoint.verify()?;
1406 Ok(checkpoint)
1407}
1408
1409fn decode_error(message: &str) -> CheckpointError {
1415 if message.contains(CHECKPOINT_ERROR_MARKER) {
1416 for code in [
1417 KernelFaultCode::CheckpointIncompatible,
1418 KernelFaultCode::CheckpointCorrupted,
1419 ] {
1420 if message.contains(&format!("{CHECKPOINT_ERROR_MARKER} ({})", code.as_str())) {
1421 return match code {
1422 KernelFaultCode::CheckpointIncompatible => {
1423 CheckpointError::Incompatible(message.to_string())
1424 }
1425 _ => CheckpointError::Corrupted(message.to_string()),
1426 };
1427 }
1428 }
1429 return CheckpointError::Corrupted(message.to_string());
1430 }
1431 if message.contains(SCALAR_ERROR_MARKER) && message.contains("ABI revision") {
1432 return CheckpointError::Incompatible(message.to_string());
1433 }
1434 CheckpointError::NotCanonical(format!("checkpoint does not decode: {message}"))
1435}
1436
1437impl<'de> Deserialize<'de> for KernelCheckpoint {
1438 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1439 let document = serde_json::Value::deserialize(deserializer)?;
1440 decode_checkpoint_value(document)
1441 .map_err(|error| serde::de::Error::custom(error.to_string()))
1442 }
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447 use std::collections::BTreeMap;
1448 use std::fs;
1449 use std::path::PathBuf;
1450
1451 use serde_json::Value;
1452
1453 use super::super::config::{ConfigDefaults, HostEffectSupport, OperationConfig};
1454 use super::super::effect::EffectKindTag;
1455 use super::super::envelope::{ConfigureOperation, KernelInput, WireEnvelope};
1456 use super::*;
1457
1458 const OPERATION: &str = "op-checkpoint-1";
1463
1464 fn operation() -> OperationId {
1465 OperationId::new(OPERATION).unwrap()
1466 }
1467
1468 fn digest(label: &str) -> Digest {
1469 canonical_digest(label.as_bytes())
1470 }
1471
1472 fn normalized(input_id: &str, at: u64) -> NormalizedInput {
1473 let envelope = WireEnvelope::new(
1474 operation(),
1475 InputId::new(input_id).unwrap(),
1476 WireU64::new(at),
1477 KernelInput::ConfigureOperation(ConfigureOperation {
1478 config: OperationConfig {
1479 host_effect_support: HostEffectSupport {
1480 supported: vec![EffectKindTag::CallProvider],
1481 },
1482 ..OperationConfig::default()
1483 },
1484 }),
1485 );
1486 NormalizedInput::normalize(&envelope, &ConfigDefaults::default()).expect("normalizes")
1487 }
1488
1489 fn tail_entry(step_seq: u64) -> CanonicalInput {
1490 CanonicalInput {
1491 step_seq: WireU64::new(step_seq),
1492 record_digest: digest(&format!("record-{step_seq}")),
1493 input: normalized(&format!("in-{step_seq}"), 1_700_000_000_000 + step_seq),
1494 }
1495 }
1496
1497 fn resolved_config() -> ResolvedOperationConfig {
1498 OperationConfig {
1499 host_effect_support: HostEffectSupport {
1500 supported: vec![EffectKindTag::CallProvider],
1501 },
1502 ..OperationConfig::default()
1503 }
1504 .resolve(&ConfigDefaults::default())
1505 .expect("the default configuration resolves")
1506 }
1507
1508 fn logical_state() -> LogicalKernelState {
1509 LogicalKernelState {
1510 transition: TransitionState {
1511 lifecycle: OperationLifecycle::Running,
1512 resolved_config: resolved_config(),
1513 root_kind: Some(RootKind::Agent),
1514 focus: None,
1515 last_observed_at_ms: WireU64::new(1_700_000_002_000),
1516 pending_effects: Vec::new(),
1517 resolved_effects: Vec::new(),
1518 launch_tokens: Vec::new(),
1519 accepted_inputs: vec![AcceptedInputState {
1520 input_id: InputId::new("in-configure").unwrap(),
1521 step_seq: WireU64::ZERO,
1522 record_digest: digest("record-0"),
1523 }],
1524 accepted_cancellation: None,
1525 terminal: None,
1526 },
1527 syscall: SyscallState::default(),
1528 scheduler: SchedulerState::default(),
1529 context_vm: ContextVmState::default(),
1530 }
1531 }
1532
1533 fn draft(base: u64, through: u64, tail: Vec<CanonicalInput>) -> CheckpointDraft {
1534 CheckpointDraft {
1535 operation_id: operation(),
1536 genesis_digest: digest("genesis"),
1537 base_step_seq: WireU64::new(base),
1538 base_record_digest: digest(&format!("record-{base}")),
1539 through_step_seq: WireU64::new(through),
1540 covered_transaction_head_digest: digest(&format!("record-{through}")),
1541 logical_state: logical_state(),
1542 tail_inputs: tail,
1543 }
1544 }
1545
1546 fn checkpoint() -> KernelCheckpoint {
1547 KernelCheckpoint::assemble(draft(3, 3, Vec::new())).expect("assembles")
1548 }
1549
1550 fn tampered(edit: impl FnOnce(&mut serde_json::Map<String, Value>)) -> CheckpointError {
1553 let mut document: serde_json::Map<String, Value> =
1554 serde_json::from_slice(checkpoint().checkpoint_bytes().as_slice()).unwrap();
1555 edit(&mut document);
1556 let bytes = serde_json::to_vec(&document).unwrap();
1557 KernelCheckpoint::from_checkpoint_bytes(&bytes)
1558 .expect_err("a tampered checkpoint must not decode")
1559 }
1560
1561 #[test]
1566 fn a_checkpoint_has_no_version_axis() {
1567 let document: Value =
1568 serde_json::from_slice(checkpoint().checkpoint_bytes().as_slice()).unwrap();
1569 assert!(document.get("checkpoint_version").is_none());
1570 assert!(document.get("abi_version").is_none());
1571 }
1572
1573 #[test]
1576 fn single_ownership_is_structural() {
1577 let checkpoint = checkpoint();
1578 let document: Value =
1579 serde_json::from_slice(checkpoint.checkpoint_bytes().as_slice()).unwrap();
1580 let state = &document["logical_state"];
1581
1582 for (owned, owner) in [
1586 ("pending_effects", "transition"),
1587 ("resolved_effects", "transition"),
1588 ("launch_tokens", "transition"),
1589 ("accepted_inputs", "transition"),
1590 ("accepted_cancellation", "transition"),
1591 ("terminal", "transition"),
1592 ("attempts", "scheduler"),
1593 ("tasks", "scheduler"),
1594 ("handles", "context_vm"),
1595 ("pending_payload_loads", "context_vm"),
1596 ("provider_calls", "syscall"),
1597 ] {
1598 let mut seen = Vec::new();
1599 for partition in ["transition", "syscall", "scheduler", "context_vm"] {
1600 if state[partition]
1601 .as_object()
1602 .map(|map| map.contains_key(owned))
1603 .unwrap_or(false)
1604 {
1605 seen.push(partition);
1606 }
1607 }
1608 assert_eq!(
1609 seen,
1610 vec![owner],
1611 "{owned} must live in exactly one partition"
1612 );
1613 }
1614
1615 let mut home: BTreeMap<String, &str> = BTreeMap::new();
1617 for partition in ["transition", "syscall", "scheduler", "context_vm"] {
1618 for key in state[partition]
1619 .as_object()
1620 .expect("a partition object")
1621 .keys()
1622 {
1623 if let Some(previous) = home.insert(key.clone(), partition) {
1624 panic!("key {key} lives in both {previous} and {partition}");
1625 }
1626 }
1627 }
1628
1629 let header: Vec<&String> = document
1631 .as_object()
1632 .unwrap()
1633 .keys()
1634 .filter(|key| home.contains_key(*key))
1635 .collect();
1636 assert!(
1637 header.is_empty(),
1638 "the checkpoint header duplicates sub-state: {header:?}"
1639 );
1640 }
1641
1642 #[test]
1645 fn the_dto_is_constructible_without_any_state_machine() {
1646 let state = LogicalKernelState {
1647 transition: TransitionState {
1648 lifecycle: OperationLifecycle::Created,
1649 resolved_config: resolved_config(),
1650 root_kind: None,
1651 focus: None,
1652 last_observed_at_ms: WireU64::ZERO,
1653 pending_effects: Vec::new(),
1654 resolved_effects: Vec::new(),
1655 launch_tokens: Vec::new(),
1656 accepted_inputs: Vec::new(),
1657 accepted_cancellation: None,
1658 terminal: None,
1659 },
1660 syscall: SyscallState::default(),
1661 scheduler: SchedulerState::default(),
1662 context_vm: ContextVmState::default(),
1663 };
1664 let mut draft = draft(0, 0, Vec::new());
1665 draft.logical_state = state;
1666 KernelCheckpoint::assemble(draft).expect("an empty logical state is still a checkpoint");
1667 }
1668
1669 #[test]
1674 fn the_three_digests_summarise_three_different_things() {
1675 let checkpoint = checkpoint();
1676 assert_eq!(
1677 checkpoint.state_digest(),
1678 &canonical_digest(
1679 canonical_bytes(checkpoint.logical_state())
1680 .unwrap()
1681 .as_slice()
1682 ),
1683 );
1684 assert_eq!(
1685 checkpoint.tail_digest(),
1686 &canonical_digest(
1687 canonical_bytes(checkpoint.tail_inputs())
1688 .unwrap()
1689 .as_slice()
1690 ),
1691 );
1692 assert_ne!(checkpoint.state_digest(), checkpoint.checkpoint_digest());
1693 assert_ne!(checkpoint.tail_digest(), checkpoint.checkpoint_digest());
1694 checkpoint
1695 .verify()
1696 .expect("a freshly built checkpoint verifies");
1697 }
1698
1699 #[test]
1702 fn the_checkpoint_digest_covers_the_header_and_the_bounded_tail() {
1703 let with_tail = KernelCheckpoint::assemble(draft(3, 4, vec![tail_entry(4)])).unwrap();
1704 let without_tail = checkpoint();
1705 assert_eq!(
1706 with_tail.state_digest(),
1707 without_tail.state_digest(),
1708 "the same logical state digests the same either way"
1709 );
1710 assert_ne!(
1711 with_tail.checkpoint_digest(),
1712 without_tail.checkpoint_digest(),
1713 "but the checkpoint digest moves with the tail and the header"
1714 );
1715
1716 let error = tampered(|document| {
1717 document.insert("through_step_seq".to_string(), Value::String("9".into()));
1718 });
1719 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1720 }
1721
1722 #[test]
1723 fn a_checkpoint_round_trips_through_its_bytes() {
1724 let original = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)]))
1725 .expect("a bounded-tail checkpoint assembles");
1726 let decoded =
1727 KernelCheckpoint::from_checkpoint_bytes(original.checkpoint_bytes().as_slice())
1728 .expect("its own bytes decode");
1729 assert_eq!(decoded, original);
1730 assert_eq!(decoded.tail_inputs().len(), 2);
1731 }
1732
1733 #[test]
1734 fn structured_message_body_rejects_removed_body_forms() {
1735 assert!(
1736 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1737 "content_json": "{\\\"Text\\\":\\\"hello\\\"}"
1738 }))
1739 .is_err()
1740 );
1741 assert!(
1742 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1743 "schema_version": 1,
1744 "durable_content": {"blocks": []}
1745 }))
1746 .is_err()
1747 );
1748 }
1749
1750 #[test]
1751 fn structured_message_body_rejects_unknown_fields() {
1752 assert!(
1753 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1754 "durable_content": {"blocks": []},
1755 "unknown": true,
1756 }))
1757 .is_err()
1758 );
1759 }
1760
1761 #[test]
1762 fn removed_durable_content_schema_field_is_not_readable() {
1763 assert!(
1764 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1765 "durable_content": {"schema_version": 1, "blocks": []}
1766 }))
1767 .is_err()
1768 );
1769 }
1770
1771 #[test]
1772 fn checkpoint_rejects_durable_tool_result_with_a_second_body_form() {
1773 let mut draft = draft(3, 3, Vec::new());
1774 draft
1775 .logical_state
1776 .context_vm
1777 .messages
1778 .push(StoredMessageState {
1779 partition: MessagePartition::History,
1780 role: "tool".into(),
1781 body: StoredMessageBody::Structured(StructuredMessageBody {
1782 durable_content: Some(crate::types::durable_content::DurableContent::text(
1783 "wrong",
1784 )),
1785 durable_tool_results: vec![
1786 crate::types::durable_content::DurableToolResult::text(
1787 "call-1",
1788 "also wrong",
1789 false,
1790 ),
1791 ],
1792 }),
1793 tool_calls: Vec::new(),
1794 tokens: 0,
1795 });
1796 assert!(matches!(
1797 KernelCheckpoint::assemble(draft),
1798 Err(CheckpointError::Incompatible(_))
1799 ));
1800 }
1801
1802 #[test]
1807 fn a_digest_that_does_not_match_its_bytes_is_corruption() {
1808 for field in ["state_digest", "tail_digest", "checkpoint_digest"] {
1809 let error = tampered(|document| {
1810 document.insert(
1811 field.to_string(),
1812 Value::String(digest("bogus").to_string()),
1813 );
1814 });
1815 assert_eq!(
1816 error.code(),
1817 KernelFaultCode::CheckpointCorrupted,
1818 "{field} must fail closed"
1819 );
1820 assert!(
1821 error.to_string().contains(CHECKPOINT_ERROR_MARKER),
1822 "{field}: every rejection carries the classifier marker"
1823 );
1824 }
1825 }
1826
1827 #[test]
1828 fn a_logical_state_edited_after_the_fact_is_corruption() {
1829 let error = tampered(|document| {
1830 document["logical_state"]["transition"]["lifecycle"] = Value::String("failed".into());
1831 });
1832 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1833 assert!(error.message().contains("logical state hashes to"));
1834 }
1835
1836 #[test]
1837 fn removed_version_fields_are_malformed() {
1838 for field in ["checkpoint_version", "abi_version"] {
1839 let error = tampered(|document| {
1840 document.insert(field.to_string(), Value::from(1));
1841 });
1842 assert_eq!(error.code(), KernelFaultCode::MalformedEnvelope);
1843 }
1844 }
1845
1846 #[test]
1847 fn an_unknown_field_is_refused_rather_than_ignored() {
1848 let error = tampered(|document| {
1849 document.insert("last_step".to_string(), Value::Null);
1851 });
1852 assert_eq!(error.code(), KernelFaultCode::MalformedEnvelope);
1853 }
1854
1855 #[test]
1856 fn a_checkpoint_from_another_operation_or_genesis_is_incompatible() {
1857 let checkpoint = checkpoint();
1858 let other = OperationId::new("op-checkpoint-2").unwrap();
1859
1860 let error = checkpoint
1861 .verify_belongs_to(&other, &digest("genesis"))
1862 .expect_err("another operation's checkpoint is not installable");
1863 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1864 assert!(error.message().contains("belongs to operation"));
1865
1866 let error = checkpoint
1867 .verify_belongs_to(&operation(), &digest("another-genesis"))
1868 .expect_err("a different genesis is a different operation");
1869 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1870 assert!(error.message().contains("binds genesis"));
1871
1872 checkpoint
1873 .verify_belongs_to(&operation(), &digest("genesis"))
1874 .expect("its own operation and genesis are accepted");
1875 }
1876
1877 #[test]
1882 fn a_tail_that_covers_the_range_exactly_is_accepted() {
1883 KernelCheckpoint::assemble(draft(0, 0, Vec::new())).expect("an empty range needs no tail");
1884 KernelCheckpoint::assemble(draft(
1885 2,
1886 5,
1887 vec![tail_entry(3), tail_entry(4), tail_entry(5)],
1888 ))
1889 .expect("(2, 5] is three contiguous inputs");
1890 }
1891
1892 #[test]
1893 fn a_tail_with_a_hole_is_refused() {
1894 let error = KernelCheckpoint::assemble(draft(
1895 2,
1896 5,
1897 vec![tail_entry(3), tail_entry(5), tail_entry(6)],
1898 ))
1899 .expect_err("step 4 is missing");
1900 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1901 assert!(error.message().contains("step 4 was due"), "{error}");
1902 }
1903
1904 #[test]
1905 fn a_tail_with_a_duplicate_is_refused() {
1906 let error = KernelCheckpoint::assemble(draft(
1907 2,
1908 5,
1909 vec![tail_entry(3), tail_entry(3), tail_entry(4)],
1910 ))
1911 .expect_err("step 3 appears twice");
1912 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1913 assert!(error.message().contains("contiguous range"), "{error}");
1914 }
1915
1916 #[test]
1917 fn a_tail_entry_outside_the_range_is_refused() {
1918 let error = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(2), tail_entry(3)]))
1920 .expect_err("step 2 is the base, not part of (2, 4]");
1921 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1922
1923 let error = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(9)]))
1925 .expect_err("step 9 is past the covered head");
1926 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1927 }
1928
1929 #[test]
1930 fn a_tail_whose_length_disagrees_with_the_range_is_refused() {
1931 let error = KernelCheckpoint::assemble(draft(2, 5, vec![tail_entry(3)]))
1932 .expect_err("(2, 5] is three inputs, not one");
1933 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1934 assert!(error.message().contains("bounded tail holds 1"), "{error}");
1935
1936 let error = KernelCheckpoint::assemble(draft(4, 2, Vec::new()))
1937 .expect_err("a base past the covered head is not a range at all");
1938 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1939 }
1940
1941 #[test]
1942 fn a_tail_input_from_another_operation_is_incompatible() {
1943 let mut foreign = tail_entry(3);
1944 foreign.input.operation_id = OperationId::new("op-checkpoint-2").unwrap();
1945 let error = KernelCheckpoint::assemble(draft(2, 3, vec![foreign]))
1946 .expect_err("a tail assembled from two journals is not a checkpoint");
1947 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1948 }
1949
1950 #[test]
1953 fn a_tail_edited_in_storage_is_refused_at_decode() {
1954 let original = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)]))
1955 .expect("assembles");
1956 let mut document: serde_json::Map<String, Value> =
1957 serde_json::from_slice(original.checkpoint_bytes().as_slice()).unwrap();
1958 let tail = document["tail_inputs"].as_array_mut().unwrap();
1959 tail.remove(0);
1960 let bytes = serde_json::to_vec(&document).unwrap();
1961 let error = KernelCheckpoint::from_checkpoint_bytes(&bytes)
1962 .expect_err("a truncated tail no longer covers its range");
1963 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1964 }
1965
1966 #[test]
1971 fn a_candidate_carries_the_five_values_of_the_spec_arrow() {
1972 let checkpoint = KernelCheckpoint::assemble(draft(3, 4, vec![tail_entry(4)])).unwrap();
1973 let expected_digest = checkpoint.checkpoint_digest().clone();
1974 let candidate = checkpoint.into_candidate();
1975
1976 assert_eq!(candidate.through_step_seq, WireU64::new(4));
1977 assert_eq!(candidate.covered_head, digest("record-4"));
1978 assert!(
1979 candidate.ack_token.as_str().contains(OPERATION)
1980 && candidate
1981 .ack_token
1982 .as_str()
1983 .contains(expected_digest.as_str()),
1984 "the ack token names the checkpoint it acknowledges: {}",
1985 candidate.ack_token
1986 );
1987
1988 let decoded = candidate.decode().expect("the blob decodes and verifies");
1989 assert_eq!(decoded.checkpoint_digest(), &expected_digest);
1990 assert_eq!(decoded.state_digest(), &candidate.state_digest);
1991 assert_eq!(
1992 candidate.boundary().through_step_seq,
1993 candidate.through_step_seq
1994 );
1995 }
1996
1997 fn fixture_dir() -> PathBuf {
2002 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
2003 }
2004
2005 #[test]
2014 fn bless_checkpoint_rejection_fixtures() {
2015 if std::env::var("BLESS_KERNEL_RECORD_FIXTURES").as_deref() != Ok("1") {
2016 return;
2017 }
2018 let dir = fixture_dir();
2019 for (name, expect, description, mutate) in rejection_cases() {
2020 let mut document: serde_json::Map<String, Value> =
2021 serde_json::from_slice(mutate.0.checkpoint_bytes().as_slice()).unwrap();
2022 (mutate.1)(&mut document);
2023 let fixture = serde_json::json!({
2024 "expect": expect,
2025 "description": description,
2026 "checkpoint": Value::Object(document),
2027 });
2028 let mut text = serde_json::to_string_pretty(&fixture).unwrap();
2029 text.push('\n');
2030 fs::write(dir.join(name), text).unwrap_or_else(|e| panic!("cannot bless {name}: {e}"));
2031 }
2032 }
2033
2034 #[allow(clippy::type_complexity)]
2035 fn rejection_cases() -> Vec<(
2036 &'static str,
2037 &'static str,
2038 &'static str,
2039 (
2040 KernelCheckpoint,
2041 Box<dyn Fn(&mut serde_json::Map<String, Value>)>,
2042 ),
2043 )> {
2044 let full = || checkpoint();
2045 let with_tail =
2046 || KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)])).unwrap();
2047 vec![
2048 (
2049 "reject_checkpoint_removed_checkpoint_version.json",
2050 "malformed_envelope",
2051 "A checkpoint carrying the removed checkpoint version field is refused at the \
2052 strict decode boundary.",
2053 (
2054 full(),
2055 Box::new(|d: &mut serde_json::Map<String, Value>| {
2056 d.insert("checkpoint_version".into(), Value::from(99u64));
2057 }) as Box<dyn Fn(&mut serde_json::Map<String, Value>)>,
2058 ),
2059 ),
2060 (
2061 "reject_checkpoint_removed_abi_version.json",
2062 "malformed_envelope",
2063 "A checkpoint carrying the removed ABI version field is refused at the strict \
2064 decode boundary.",
2065 (
2066 full(),
2067 Box::new(|d: &mut serde_json::Map<String, Value>| {
2068 d.insert("abi_version".into(), Value::from(1));
2069 }),
2070 ),
2071 ),
2072 (
2073 "reject_checkpoint_state_digest_mismatch.json",
2074 "checkpoint_corrupted",
2075 "The logical state does not hash to the digest the checkpoint claims (spec 12.1).",
2076 (
2077 full(),
2078 Box::new(|d: &mut serde_json::Map<String, Value>| {
2079 d.insert(
2080 "state_digest".into(),
2081 Value::String(digest("bogus").to_string()),
2082 );
2083 }),
2084 ),
2085 ),
2086 (
2087 "reject_checkpoint_missing_field_checkpoint_digest.json",
2088 "malformed_envelope",
2089 "A structural refusal that names the field: a checkpoint without its own digest is \
2090 not a checkpoint with an unverified digest.",
2091 (
2092 full(),
2093 Box::new(|d: &mut serde_json::Map<String, Value>| {
2094 d.remove("checkpoint_digest");
2095 }),
2096 ),
2097 ),
2098 (
2099 "reject_checkpoint_unknown_field_last_step.json",
2100 "malformed_envelope",
2101 "Spec 12.4 deleted `last_step`; a blob that still carries one is refused \
2102 rather than partially read.",
2103 (
2104 full(),
2105 Box::new(|d: &mut serde_json::Map<String, Value>| {
2106 d.insert("last_step".into(), Value::Null);
2107 }),
2108 ),
2109 ),
2110 (
2111 "reject_checkpoint_base_disagrees_with_covered_head.json",
2112 "checkpoint_corrupted",
2113 "A full-state checkpoint covers no tail, so its base and its covered head name the \
2114 same record; a header that disagrees with itself would hand a restore two \
2115 different chain anchors (spec 12.1, Task 16).",
2116 (
2117 full(),
2118 Box::new(|d: &mut serde_json::Map<String, Value>| {
2119 d.insert(
2120 "base_record_digest".into(),
2121 Value::String(digest("another-record").to_string()),
2122 );
2123 }),
2124 ),
2125 ),
2126 (
2127 "reject_checkpoint_tail_hole.json",
2128 "checkpoint_corrupted",
2129 "The bounded tail must cover (base, through] with no hole (spec 12.1).",
2130 (
2131 with_tail(),
2132 Box::new(|d: &mut serde_json::Map<String, Value>| {
2133 d["tail_inputs"].as_array_mut().unwrap().remove(0);
2134 }),
2135 ),
2136 ),
2137 (
2138 "reject_checkpoint_tail_duplicate.json",
2139 "checkpoint_corrupted",
2140 "The bounded tail must cover (base, through] with no duplicate (spec 12.1).",
2141 (
2142 with_tail(),
2143 Box::new(|d: &mut serde_json::Map<String, Value>| {
2144 let tail = d["tail_inputs"].as_array_mut().unwrap();
2145 tail[1] = tail[0].clone();
2146 }),
2147 ),
2148 ),
2149 (
2150 "reject_checkpoint_tail_foreign_operation.json",
2151 "checkpoint_incompatible",
2152 "A bounded tail assembled from two journals is not a checkpoint (spec 12.1).",
2153 (
2154 with_tail(),
2155 Box::new(|d: &mut serde_json::Map<String, Value>| {
2156 d["tail_inputs"][0]["input"]["operation_id"] =
2157 Value::String("op-checkpoint-2".into());
2158 }),
2159 ),
2160 ),
2161 (
2162 "reject_checkpoint_tail_ends_off_the_covered_head.json",
2163 "checkpoint_corrupted",
2164 "The last bounded-tail entry *is* the covered head; a tail that ends somewhere \
2165 else covers a different prefix than the header claims (spec 12.1, Task 16).",
2166 (
2167 with_tail(),
2168 Box::new(|d: &mut serde_json::Map<String, Value>| {
2169 d["tail_inputs"][1]["record_digest"] =
2170 Value::String(digest("some-other-record").to_string());
2171 }),
2172 ),
2173 ),
2174 ]
2175 }
2176
2177 #[test]
2178 fn checkpoint_rejection_fixtures_fail_closed_with_the_declared_kind() {
2179 let dir = fixture_dir();
2180 let mut names: Vec<String> = fs::read_dir(&dir)
2181 .unwrap_or_else(|e| panic!("failed to read {}: {e}", dir.display()))
2182 .map(|entry| {
2183 entry
2184 .expect("dir entry")
2185 .file_name()
2186 .to_string_lossy()
2187 .to_string()
2188 })
2189 .filter(|name| name.starts_with("reject_checkpoint_") && name.ends_with(".json"))
2190 .collect();
2191 names.sort();
2192 assert!(
2193 names.len() >= 5,
2194 "too few checkpoint rejection fixtures: {names:?}"
2195 );
2196
2197 for name in names {
2198 let raw = fs::read_to_string(dir.join(&name)).expect("fixture reads");
2199 let fixture: Value = serde_json::from_str(&raw).expect("fixture is JSON");
2200 let expected = fixture["expect"]
2201 .as_str()
2202 .expect("every fixture declares `expect`");
2203 let bytes = serde_json::to_vec(&fixture["checkpoint"]).unwrap();
2204 let error = KernelCheckpoint::from_checkpoint_bytes(&bytes)
2205 .expect_err(&format!("{name}: expected a rejection"));
2206 assert_eq!(
2207 error.code().as_str(),
2208 expected,
2209 "{name}: {} (message: {})",
2210 error.code().as_str(),
2211 error.message()
2212 );
2213 for (marker, needle) in [
2216 ("_missing_field_", "missing field"),
2217 ("_unknown_field_", "unknown field"),
2218 ] {
2219 if name.contains(marker) {
2220 assert_eq!(
2221 error.code(),
2222 KernelFaultCode::MalformedEnvelope,
2223 "{name}: a structural refusal is malformed_envelope"
2224 );
2225 assert!(
2226 error.message().contains(needle),
2227 "{name}: the rejection must say which field ({})",
2228 error.message()
2229 );
2230 }
2231 }
2232 }
2233 }
2234}