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 handles: Vec<HandleState>,
618 pub next_handle_id: u32,
621 #[serde(default)]
625 pub pending_payload_loads: Vec<PendingPayloadLoadState>,
626 #[serde(default)]
627 pub active_skills: Vec<SkillLeaseState>,
628 #[serde(default)]
629 pub knowledge: Vec<KnowledgeSlotState>,
630 #[serde(default)]
631 pub signals: Vec<String>,
632 #[serde(default)]
637 pub messages: Vec<StoredMessageState>,
638 pub task_state: LogicalTaskState,
642 pub partition_tokens: PartitionTokenState,
643 pub history_len: u32,
644 #[serde(default)]
646 pub frozen_history_len: u32,
647 pub last_activity_ms: WireU64,
648 #[serde(default)]
649 pub last_compact_ms: Option<WireU64>,
650}
651
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658pub enum MessagePartition {
659 System,
660 History,
661}
662
663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
674#[serde(deny_unknown_fields)]
675pub struct StoredMessageState {
676 pub partition: MessagePartition,
677 pub role: String,
679 pub body: StoredMessageBody,
680 #[serde(default)]
683 pub tool_calls: Vec<LogicalToolCall>,
684 pub tokens: u32,
687}
688
689#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
696#[serde(tag = "form", rename_all = "snake_case")]
697pub enum StoredMessageBody {
698 Inline(InlineMessageBody),
700 Reference(ReferencedMessageBody),
703 Structured(StructuredMessageBody),
709}
710
711#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
712#[serde(deny_unknown_fields)]
713pub struct InlineMessageBody {
714 pub text: String,
715 #[serde(default)]
717 pub tool_call_id: Option<String>,
718 #[serde(default)]
719 pub is_error: bool,
720}
721
722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
723#[serde(deny_unknown_fields)]
724pub struct ReferencedMessageBody {
725 pub handle_id: u32,
728 pub digest: String,
730 pub preview: String,
732 #[serde(default)]
733 pub tool_call_id: Option<String>,
734 #[serde(default)]
735 pub is_error: bool,
736}
737
738#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
739#[serde(deny_unknown_fields)]
740pub struct StructuredMessageBody {
741 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub durable_content: Option<crate::types::durable_content::DurableContent>,
744 #[serde(default, skip_serializing_if = "Vec::is_empty")]
747 pub durable_tool_results: Vec<crate::types::durable_content::DurableToolResult>,
748}
749
750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
751#[serde(deny_unknown_fields)]
752pub struct LogicalToolCall {
753 pub call_id: String,
754 pub name: String,
755 pub arguments: String,
758}
759
760#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
766#[serde(deny_unknown_fields)]
767pub struct LogicalTaskState {
768 #[serde(default)]
769 pub goal: String,
770 #[serde(default)]
771 pub criteria: Vec<String>,
772 #[serde(default)]
773 pub plan: Vec<LogicalPlanStep>,
774 #[serde(default)]
775 pub current_step: Option<u32>,
776 #[serde(default)]
777 pub progress: String,
778 #[serde(default)]
779 pub scratchpad: String,
780 #[serde(default)]
781 pub blocked_on: Vec<String>,
782 #[serde(default)]
783 pub directives: Vec<String>,
784 #[serde(default)]
785 pub preserved_refs: Vec<String>,
786 #[serde(default)]
787 pub recent_actions: Vec<String>,
788 #[serde(default)]
789 pub compression_log: Vec<LogicalCompressionEntry>,
790 #[serde(default)]
791 pub compression_log_dropped: WireU64,
792}
793
794#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
795#[serde(deny_unknown_fields)]
796pub struct LogicalPlanStep {
797 pub label: String,
798 pub done: bool,
799}
800
801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
802#[serde(deny_unknown_fields)]
803pub struct LogicalCompressionEntry {
804 pub action: String,
805 pub summary: String,
806}
807
808#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
811#[serde(deny_unknown_fields)]
812pub struct HandleState {
813 pub handle_id: u32,
814 pub kind: String,
815 pub residency: String,
816 #[serde(default)]
817 pub payload_ref: Option<String>,
818 #[serde(default)]
819 pub digest: Option<String>,
820 #[serde(default)]
821 pub original_size: Option<WireU64>,
822 pub tokens: u32,
823 #[serde(default)]
825 pub source: Option<String>,
826}
827
828#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
829#[serde(deny_unknown_fields)]
830pub struct PendingPayloadLoadState {
831 pub effect_id: EffectId,
832 pub handle_id: String,
833 pub digest: String,
834 #[serde(default)]
835 pub original_size: Option<WireU64>,
836}
837
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
839#[serde(deny_unknown_fields)]
840pub struct SkillLeaseState {
841 pub skill: String,
842 #[serde(default)]
844 pub lease_until_turn: Option<u32>,
845}
846
847#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
854#[serde(deny_unknown_fields)]
855pub struct KnowledgeSlotState {
856 #[serde(default)]
858 pub key: Option<String>,
859 pub role: String,
860 pub body: StoredMessageBody,
861 pub tokens: u32,
862 pub pinned: bool,
863 pub evict_at_boundary: bool,
864}
865
866#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
867#[serde(deny_unknown_fields)]
868pub struct PartitionTokenState {
869 pub system: u32,
870 pub knowledge: u32,
871 pub history: u32,
872}
873
874#[derive(Debug, Clone, PartialEq)]
880pub struct LogicalStateProjection {
881 pub root_kind: Option<RootKind>,
882 pub focus: Option<ExecutionFocus>,
883 pub syscall: SyscallState,
884 pub scheduler: SchedulerState,
885 pub context_vm: ContextVmState,
886}
887
888#[derive(Debug, Clone, PartialEq)]
895pub struct CheckpointDraft {
896 pub operation_id: OperationId,
897 pub genesis_digest: Digest,
898 pub base_step_seq: WireU64,
899 pub base_record_digest: Digest,
900 pub through_step_seq: WireU64,
901 pub covered_transaction_head_digest: Digest,
902 pub logical_state: LogicalKernelState,
903 pub tail_inputs: Vec<CanonicalInput>,
904}
905
906#[derive(Debug, Clone, PartialEq, Serialize)]
914pub struct KernelCheckpoint {
915 operation_id: OperationId,
916 genesis_digest: Digest,
919 base_step_seq: WireU64,
921 base_record_digest: Digest,
928 through_step_seq: WireU64,
930 covered_transaction_head_digest: Digest,
933 logical_state: LogicalKernelState,
934 tail_inputs: Vec<CanonicalInput>,
935 state_digest: Digest,
936 tail_digest: Digest,
937 checkpoint_digest: Digest,
938}
939
940#[derive(Serialize)]
942struct CheckpointBody<'a> {
943 operation_id: &'a OperationId,
944 genesis_digest: &'a Digest,
945 base_step_seq: WireU64,
946 base_record_digest: &'a Digest,
947 through_step_seq: WireU64,
948 covered_transaction_head_digest: &'a Digest,
949 logical_state: &'a LogicalKernelState,
950 tail_inputs: &'a [CanonicalInput],
951 state_digest: &'a Digest,
952 tail_digest: &'a Digest,
953}
954
955impl KernelCheckpoint {
956 pub fn assemble(draft: CheckpointDraft) -> Result<Self, CheckpointError> {
959 let CheckpointDraft {
960 operation_id,
961 genesis_digest,
962 base_step_seq,
963 base_record_digest,
964 through_step_seq,
965 covered_transaction_head_digest,
966 logical_state,
967 tail_inputs,
968 } = draft;
969
970 validate_durable_message_bodies(&logical_state.context_vm)?;
971
972 check_tail(
973 &operation_id,
974 base_step_seq,
975 &base_record_digest,
976 through_step_seq,
977 &covered_transaction_head_digest,
978 &tail_inputs,
979 )?;
980
981 let state_digest = canonical_digest(canonical_bytes(&logical_state)?.as_slice());
982 let tail_digest = canonical_digest(canonical_bytes(&tail_inputs)?.as_slice());
983 let checkpoint_digest = Self::body_digest(&CheckpointBody {
984 operation_id: &operation_id,
985 genesis_digest: &genesis_digest,
986 base_step_seq,
987 base_record_digest: &base_record_digest,
988 through_step_seq,
989 covered_transaction_head_digest: &covered_transaction_head_digest,
990 logical_state: &logical_state,
991 tail_inputs: &tail_inputs,
992 state_digest: &state_digest,
993 tail_digest: &tail_digest,
994 })?;
995
996 Ok(Self {
997 operation_id,
998 genesis_digest,
999 base_step_seq,
1000 base_record_digest,
1001 through_step_seq,
1002 covered_transaction_head_digest,
1003 logical_state,
1004 tail_inputs,
1005 state_digest,
1006 tail_digest,
1007 checkpoint_digest,
1008 })
1009 }
1010
1011 fn body_digest(body: &CheckpointBody<'_>) -> Result<Digest, CheckpointError> {
1012 Ok(canonical_digest(canonical_bytes(body)?.as_slice()))
1013 }
1014
1015 pub fn operation_id(&self) -> &OperationId {
1018 &self.operation_id
1019 }
1020
1021 pub fn genesis_digest(&self) -> &Digest {
1022 &self.genesis_digest
1023 }
1024
1025 pub fn base_step_seq(&self) -> WireU64 {
1026 self.base_step_seq
1027 }
1028
1029 pub fn base_record_digest(&self) -> &Digest {
1030 &self.base_record_digest
1031 }
1032
1033 pub fn through_step_seq(&self) -> WireU64 {
1034 self.through_step_seq
1035 }
1036
1037 pub fn covered_transaction_head_digest(&self) -> &Digest {
1038 &self.covered_transaction_head_digest
1039 }
1040
1041 pub fn logical_state(&self) -> &LogicalKernelState {
1042 &self.logical_state
1043 }
1044
1045 pub fn tail_inputs(&self) -> &[CanonicalInput] {
1046 &self.tail_inputs
1047 }
1048
1049 pub fn state_digest(&self) -> &Digest {
1050 &self.state_digest
1051 }
1052
1053 pub fn tail_digest(&self) -> &Digest {
1054 &self.tail_digest
1055 }
1056
1057 pub fn checkpoint_digest(&self) -> &Digest {
1058 &self.checkpoint_digest
1059 }
1060
1061 pub fn checkpoint_bytes(&self) -> CanonicalBytes {
1065 canonical_bytes(self).expect("a checkpoint contains only canonical scalars")
1066 }
1067
1068 pub fn from_checkpoint_bytes(bytes: &[u8]) -> Result<Self, CheckpointError> {
1070 let text = std::str::from_utf8(bytes).map_err(|error| {
1071 CheckpointError::NotCanonical(format!("checkpoint bytes are not UTF-8: {error}"))
1072 })?;
1073 let document =
1074 serde_json::from_str(text).map_err(|error| decode_error(&error.to_string()))?;
1075 decode_checkpoint_value(document)
1076 }
1077
1078 pub fn boundary(&self) -> super::transaction::CheckpointBoundary {
1080 super::transaction::CheckpointBoundary {
1081 through_step_seq: self.through_step_seq,
1082 covered_head: self.covered_transaction_head_digest.clone(),
1083 }
1084 }
1085
1086 pub fn into_candidate(self) -> CheckpointCandidate {
1088 let ack_token = ack_token_for(
1089 &self.operation_id,
1090 self.through_step_seq,
1091 &self.checkpoint_digest,
1092 );
1093 CheckpointCandidate {
1094 checkpoint_bytes: self.checkpoint_bytes(),
1095 through_step_seq: self.through_step_seq,
1096 covered_head: self.covered_transaction_head_digest.clone(),
1097 state_digest: self.state_digest.clone(),
1098 ack_token,
1099 }
1100 }
1101
1102 pub fn verify(&self) -> Result<(), CheckpointError> {
1109 validate_durable_message_bodies(&self.logical_state.context_vm)?;
1110 check_tail(
1111 &self.operation_id,
1112 self.base_step_seq,
1113 &self.base_record_digest,
1114 self.through_step_seq,
1115 &self.covered_transaction_head_digest,
1116 &self.tail_inputs,
1117 )?;
1118
1119 let state_digest = canonical_digest(canonical_bytes(&self.logical_state)?.as_slice());
1120 if state_digest != self.state_digest {
1121 return Err(CheckpointError::Corrupted(format!(
1122 "checkpoint {} through step {}: the logical state hashes to {state_digest}, \
1123 but the checkpoint claims {}",
1124 self.operation_id, self.through_step_seq, self.state_digest
1125 )));
1126 }
1127 let tail_digest = canonical_digest(canonical_bytes(&self.tail_inputs)?.as_slice());
1128 if tail_digest != self.tail_digest {
1129 return Err(CheckpointError::Corrupted(format!(
1130 "checkpoint {} through step {}: the bounded tail hashes to {tail_digest}, \
1131 but the checkpoint claims {}",
1132 self.operation_id, self.through_step_seq, self.tail_digest
1133 )));
1134 }
1135 let checkpoint_digest = Self::body_digest(&CheckpointBody {
1136 operation_id: &self.operation_id,
1137 genesis_digest: &self.genesis_digest,
1138 base_step_seq: self.base_step_seq,
1139 base_record_digest: &self.base_record_digest,
1140 through_step_seq: self.through_step_seq,
1141 covered_transaction_head_digest: &self.covered_transaction_head_digest,
1142 logical_state: &self.logical_state,
1143 tail_inputs: &self.tail_inputs,
1144 state_digest: &self.state_digest,
1145 tail_digest: &self.tail_digest,
1146 })?;
1147 if checkpoint_digest != self.checkpoint_digest {
1148 return Err(CheckpointError::Corrupted(format!(
1149 "checkpoint {} through step {}: the body hashes to {checkpoint_digest}, \
1150 but the checkpoint claims {}",
1151 self.operation_id, self.through_step_seq, self.checkpoint_digest
1152 )));
1153 }
1154 Ok(())
1155 }
1156
1157 pub fn verify_belongs_to(
1159 &self,
1160 operation_id: &OperationId,
1161 genesis_digest: &Digest,
1162 ) -> Result<(), CheckpointError> {
1163 if &self.operation_id != operation_id {
1164 return Err(CheckpointError::Incompatible(format!(
1165 "checkpoint belongs to operation {}, this runtime to {operation_id}",
1166 self.operation_id
1167 )));
1168 }
1169 if &self.genesis_digest != genesis_digest {
1170 return Err(CheckpointError::Incompatible(format!(
1171 "checkpoint {operation_id} binds genesis {}, this journal's genesis is \
1172 {genesis_digest}",
1173 self.genesis_digest
1174 )));
1175 }
1176 Ok(())
1177 }
1178}
1179
1180fn validate_durable_message_bodies(context: &ContextVmState) -> Result<(), CheckpointError> {
1181 let bodies = context
1182 .messages
1183 .iter()
1184 .map(|message| &message.body)
1185 .chain(context.knowledge.iter().map(|slot| &slot.body));
1186 for body in bodies {
1187 let StoredMessageBody::Structured(structured) = body else {
1188 continue;
1189 };
1190 let body_forms = usize::from(structured.durable_content.is_some())
1191 + usize::from(!structured.durable_tool_results.is_empty());
1192 if body_forms > 1 {
1193 return Err(CheckpointError::Incompatible(
1194 "structured message carries more than one durable body form".into(),
1195 ));
1196 }
1197 if !structured.durable_tool_results.is_empty() {
1198 for result in &structured.durable_tool_results {
1199 result.validate().map_err(|error| {
1200 CheckpointError::Incompatible(format!(
1201 "structured message carries invalid durable tool result: {error}"
1202 ))
1203 })?;
1204 }
1205 } else if let Some(content) = &structured.durable_content {
1206 content.validate().map_err(|error| {
1207 CheckpointError::Incompatible(format!(
1208 "structured message carries invalid durable content: {error}"
1209 ))
1210 })?;
1211 } else {
1212 return Err(CheckpointError::Incompatible(
1213 "structured message carries no durable content".into(),
1214 ));
1215 }
1216 }
1217 Ok(())
1218}
1219
1220fn check_tail(
1227 operation_id: &OperationId,
1228 base_step_seq: WireU64,
1229 base_record_digest: &Digest,
1230 through_step_seq: WireU64,
1231 covered_transaction_head_digest: &Digest,
1232 tail_inputs: &[CanonicalInput],
1233) -> Result<(), CheckpointError> {
1234 if base_step_seq > through_step_seq {
1235 return Err(CheckpointError::Corrupted(format!(
1236 "checkpoint {operation_id} bases at step {base_step_seq} but covers only through \
1237 {through_step_seq}"
1238 )));
1239 }
1240 if base_step_seq == through_step_seq && base_record_digest != covered_transaction_head_digest {
1244 return Err(CheckpointError::Corrupted(format!(
1245 "checkpoint {operation_id} covers no tail, so its base {base_record_digest} and its \
1246 covered head {covered_transaction_head_digest} name the same record — but they differ"
1247 )));
1248 }
1249 let expected = through_step_seq.get() - base_step_seq.get();
1250 if tail_inputs.len() as u64 != expected {
1251 return Err(CheckpointError::Corrupted(format!(
1252 "checkpoint {operation_id} covers ({base_step_seq}, {through_step_seq}] — {expected} \
1253 inputs — but its bounded tail holds {}",
1254 tail_inputs.len()
1255 )));
1256 }
1257 for (offset, entry) in tail_inputs.iter().enumerate() {
1258 let want = base_step_seq.get() + offset as u64 + 1;
1259 if entry.step_seq.get() != want {
1260 return Err(CheckpointError::Corrupted(format!(
1261 "checkpoint {operation_id} bounded tail is not the contiguous range \
1262 ({base_step_seq}, {through_step_seq}]: position {offset} is step {} where step \
1263 {want} was due",
1264 entry.step_seq
1265 )));
1266 }
1267 if &entry.input.operation_id != operation_id {
1268 return Err(CheckpointError::Incompatible(format!(
1269 "checkpoint {operation_id} bounded tail carries an input of operation {} at step \
1270 {}",
1271 entry.input.operation_id, entry.step_seq
1272 )));
1273 }
1274 }
1275 if let Some(last) = tail_inputs.last()
1278 && &last.record_digest != covered_transaction_head_digest
1279 {
1280 return Err(CheckpointError::Corrupted(format!(
1281 "checkpoint {operation_id} claims covered head {covered_transaction_head_digest}, but \
1282 its bounded tail ends at {} on step {}",
1283 last.record_digest, last.step_seq
1284 )));
1285 }
1286 Ok(())
1287}
1288
1289fn ack_token_for(
1290 operation_id: &OperationId,
1291 through_step_seq: WireU64,
1292 checkpoint_digest: &Digest,
1293) -> CheckpointAckToken {
1294 CheckpointAckToken::new(format!(
1295 "{operation_id}:checkpoint:{through_step_seq}:{checkpoint_digest}"
1296 ))
1297 .expect("an operation-scoped checkpoint ack token is always a legal branded ref")
1298}
1299
1300#[derive(Debug, Clone, PartialEq)]
1307pub struct CheckpointCandidate {
1308 pub checkpoint_bytes: CanonicalBytes,
1309 pub through_step_seq: WireU64,
1310 pub covered_head: Digest,
1311 pub state_digest: Digest,
1312 pub ack_token: CheckpointAckToken,
1313}
1314
1315impl CheckpointCandidate {
1316 pub fn boundary(&self) -> super::transaction::CheckpointBoundary {
1318 super::transaction::CheckpointBoundary {
1319 through_step_seq: self.through_step_seq,
1320 covered_head: self.covered_head.clone(),
1321 }
1322 }
1323
1324 pub fn decode(&self) -> Result<KernelCheckpoint, CheckpointError> {
1327 KernelCheckpoint::from_checkpoint_bytes(self.checkpoint_bytes.as_slice())
1328 }
1329}
1330
1331#[derive(Deserialize)]
1339#[serde(deny_unknown_fields)]
1340struct CheckpointProjection {
1341 operation_id: OperationId,
1342 genesis_digest: Digest,
1343 base_step_seq: WireU64,
1344 base_record_digest: Digest,
1345 through_step_seq: WireU64,
1346 covered_transaction_head_digest: Digest,
1347 logical_state: LogicalKernelState,
1348 tail_inputs: Vec<CanonicalInput>,
1349 state_digest: Digest,
1350 tail_digest: Digest,
1351 checkpoint_digest: Digest,
1352}
1353
1354fn decode_checkpoint_value(
1355 document: serde_json::Value,
1356) -> Result<KernelCheckpoint, CheckpointError> {
1357 decode_current_checkpoint(document)
1358}
1359
1360fn decode_current_checkpoint(
1361 document: serde_json::Value,
1362) -> Result<KernelCheckpoint, CheckpointError> {
1363 let projection = serde_json::from_value::<CheckpointProjection>(document)
1364 .map_err(|error| decode_error(&error.to_string()))?;
1365 let checkpoint = KernelCheckpoint {
1366 operation_id: projection.operation_id,
1367 genesis_digest: projection.genesis_digest,
1368 base_step_seq: projection.base_step_seq,
1369 base_record_digest: projection.base_record_digest,
1370 through_step_seq: projection.through_step_seq,
1371 covered_transaction_head_digest: projection.covered_transaction_head_digest,
1372 logical_state: projection.logical_state,
1373 tail_inputs: projection.tail_inputs,
1374 state_digest: projection.state_digest,
1375 tail_digest: projection.tail_digest,
1376 checkpoint_digest: projection.checkpoint_digest,
1377 };
1378 checkpoint.verify()?;
1379 Ok(checkpoint)
1380}
1381
1382fn decode_error(message: &str) -> CheckpointError {
1388 if message.contains(CHECKPOINT_ERROR_MARKER) {
1389 for code in [
1390 KernelFaultCode::CheckpointIncompatible,
1391 KernelFaultCode::CheckpointCorrupted,
1392 ] {
1393 if message.contains(&format!("{CHECKPOINT_ERROR_MARKER} ({})", code.as_str())) {
1394 return match code {
1395 KernelFaultCode::CheckpointIncompatible => {
1396 CheckpointError::Incompatible(message.to_string())
1397 }
1398 _ => CheckpointError::Corrupted(message.to_string()),
1399 };
1400 }
1401 }
1402 return CheckpointError::Corrupted(message.to_string());
1403 }
1404 if message.contains(SCALAR_ERROR_MARKER) && message.contains("ABI revision") {
1405 return CheckpointError::Incompatible(message.to_string());
1406 }
1407 CheckpointError::NotCanonical(format!("checkpoint does not decode: {message}"))
1408}
1409
1410impl<'de> Deserialize<'de> for KernelCheckpoint {
1411 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1412 let document = serde_json::Value::deserialize(deserializer)?;
1413 decode_checkpoint_value(document)
1414 .map_err(|error| serde::de::Error::custom(error.to_string()))
1415 }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420 use std::collections::BTreeMap;
1421 use std::fs;
1422 use std::path::PathBuf;
1423
1424 use serde_json::Value;
1425
1426 use super::super::config::{ConfigDefaults, HostEffectSupport, OperationConfig};
1427 use super::super::effect::EffectKindTag;
1428 use super::super::envelope::{ConfigureOperation, KernelInput, WireEnvelope};
1429 use super::*;
1430
1431 const OPERATION: &str = "op-checkpoint-1";
1436
1437 fn operation() -> OperationId {
1438 OperationId::new(OPERATION).unwrap()
1439 }
1440
1441 fn digest(label: &str) -> Digest {
1442 canonical_digest(label.as_bytes())
1443 }
1444
1445 fn normalized(input_id: &str, at: u64) -> NormalizedInput {
1446 let envelope = WireEnvelope::new(
1447 operation(),
1448 InputId::new(input_id).unwrap(),
1449 WireU64::new(at),
1450 KernelInput::ConfigureOperation(ConfigureOperation {
1451 config: OperationConfig {
1452 host_effect_support: HostEffectSupport {
1453 supported: vec![EffectKindTag::CallProvider],
1454 },
1455 ..OperationConfig::default()
1456 },
1457 }),
1458 );
1459 NormalizedInput::normalize(&envelope, &ConfigDefaults::default()).expect("normalizes")
1460 }
1461
1462 fn tail_entry(step_seq: u64) -> CanonicalInput {
1463 CanonicalInput {
1464 step_seq: WireU64::new(step_seq),
1465 record_digest: digest(&format!("record-{step_seq}")),
1466 input: normalized(&format!("in-{step_seq}"), 1_700_000_000_000 + step_seq),
1467 }
1468 }
1469
1470 fn resolved_config() -> ResolvedOperationConfig {
1471 OperationConfig {
1472 host_effect_support: HostEffectSupport {
1473 supported: vec![EffectKindTag::CallProvider],
1474 },
1475 ..OperationConfig::default()
1476 }
1477 .resolve(&ConfigDefaults::default())
1478 .expect("the default configuration resolves")
1479 }
1480
1481 fn logical_state() -> LogicalKernelState {
1482 LogicalKernelState {
1483 transition: TransitionState {
1484 lifecycle: OperationLifecycle::Running,
1485 resolved_config: resolved_config(),
1486 root_kind: Some(RootKind::Agent),
1487 focus: None,
1488 last_observed_at_ms: WireU64::new(1_700_000_002_000),
1489 pending_effects: Vec::new(),
1490 resolved_effects: Vec::new(),
1491 launch_tokens: Vec::new(),
1492 accepted_inputs: vec![AcceptedInputState {
1493 input_id: InputId::new("in-configure").unwrap(),
1494 step_seq: WireU64::ZERO,
1495 record_digest: digest("record-0"),
1496 }],
1497 accepted_cancellation: None,
1498 terminal: None,
1499 },
1500 syscall: SyscallState::default(),
1501 scheduler: SchedulerState::default(),
1502 context_vm: ContextVmState::default(),
1503 }
1504 }
1505
1506 fn draft(base: u64, through: u64, tail: Vec<CanonicalInput>) -> CheckpointDraft {
1507 CheckpointDraft {
1508 operation_id: operation(),
1509 genesis_digest: digest("genesis"),
1510 base_step_seq: WireU64::new(base),
1511 base_record_digest: digest(&format!("record-{base}")),
1512 through_step_seq: WireU64::new(through),
1513 covered_transaction_head_digest: digest(&format!("record-{through}")),
1514 logical_state: logical_state(),
1515 tail_inputs: tail,
1516 }
1517 }
1518
1519 fn checkpoint() -> KernelCheckpoint {
1520 KernelCheckpoint::assemble(draft(3, 3, Vec::new())).expect("assembles")
1521 }
1522
1523 fn tampered(edit: impl FnOnce(&mut serde_json::Map<String, Value>)) -> CheckpointError {
1526 let mut document: serde_json::Map<String, Value> =
1527 serde_json::from_slice(checkpoint().checkpoint_bytes().as_slice()).unwrap();
1528 edit(&mut document);
1529 let bytes = serde_json::to_vec(&document).unwrap();
1530 KernelCheckpoint::from_checkpoint_bytes(&bytes)
1531 .expect_err("a tampered checkpoint must not decode")
1532 }
1533
1534 #[test]
1539 fn a_checkpoint_has_no_version_axis() {
1540 let document: Value =
1541 serde_json::from_slice(checkpoint().checkpoint_bytes().as_slice()).unwrap();
1542 assert!(document.get("checkpoint_version").is_none());
1543 assert!(document.get("abi_version").is_none());
1544 }
1545
1546 #[test]
1549 fn single_ownership_is_structural() {
1550 let checkpoint = checkpoint();
1551 let document: Value =
1552 serde_json::from_slice(checkpoint.checkpoint_bytes().as_slice()).unwrap();
1553 let state = &document["logical_state"];
1554
1555 for (owned, owner) in [
1559 ("pending_effects", "transition"),
1560 ("resolved_effects", "transition"),
1561 ("launch_tokens", "transition"),
1562 ("accepted_inputs", "transition"),
1563 ("accepted_cancellation", "transition"),
1564 ("terminal", "transition"),
1565 ("attempts", "scheduler"),
1566 ("tasks", "scheduler"),
1567 ("handles", "context_vm"),
1568 ("pending_payload_loads", "context_vm"),
1569 ("provider_calls", "syscall"),
1570 ] {
1571 let mut seen = Vec::new();
1572 for partition in ["transition", "syscall", "scheduler", "context_vm"] {
1573 if state[partition]
1574 .as_object()
1575 .map(|map| map.contains_key(owned))
1576 .unwrap_or(false)
1577 {
1578 seen.push(partition);
1579 }
1580 }
1581 assert_eq!(
1582 seen,
1583 vec![owner],
1584 "{owned} must live in exactly one partition"
1585 );
1586 }
1587
1588 let mut home: BTreeMap<String, &str> = BTreeMap::new();
1590 for partition in ["transition", "syscall", "scheduler", "context_vm"] {
1591 for key in state[partition]
1592 .as_object()
1593 .expect("a partition object")
1594 .keys()
1595 {
1596 if let Some(previous) = home.insert(key.clone(), partition) {
1597 panic!("key {key} lives in both {previous} and {partition}");
1598 }
1599 }
1600 }
1601
1602 let header: Vec<&String> = document
1604 .as_object()
1605 .unwrap()
1606 .keys()
1607 .filter(|key| home.contains_key(*key))
1608 .collect();
1609 assert!(
1610 header.is_empty(),
1611 "the checkpoint header duplicates sub-state: {header:?}"
1612 );
1613 }
1614
1615 #[test]
1618 fn the_dto_is_constructible_without_any_state_machine() {
1619 let state = LogicalKernelState {
1620 transition: TransitionState {
1621 lifecycle: OperationLifecycle::Created,
1622 resolved_config: resolved_config(),
1623 root_kind: None,
1624 focus: None,
1625 last_observed_at_ms: WireU64::ZERO,
1626 pending_effects: Vec::new(),
1627 resolved_effects: Vec::new(),
1628 launch_tokens: Vec::new(),
1629 accepted_inputs: Vec::new(),
1630 accepted_cancellation: None,
1631 terminal: None,
1632 },
1633 syscall: SyscallState::default(),
1634 scheduler: SchedulerState::default(),
1635 context_vm: ContextVmState::default(),
1636 };
1637 let mut draft = draft(0, 0, Vec::new());
1638 draft.logical_state = state;
1639 KernelCheckpoint::assemble(draft).expect("an empty logical state is still a checkpoint");
1640 }
1641
1642 #[test]
1647 fn the_three_digests_summarise_three_different_things() {
1648 let checkpoint = checkpoint();
1649 assert_eq!(
1650 checkpoint.state_digest(),
1651 &canonical_digest(
1652 canonical_bytes(checkpoint.logical_state())
1653 .unwrap()
1654 .as_slice()
1655 ),
1656 );
1657 assert_eq!(
1658 checkpoint.tail_digest(),
1659 &canonical_digest(
1660 canonical_bytes(checkpoint.tail_inputs())
1661 .unwrap()
1662 .as_slice()
1663 ),
1664 );
1665 assert_ne!(checkpoint.state_digest(), checkpoint.checkpoint_digest());
1666 assert_ne!(checkpoint.tail_digest(), checkpoint.checkpoint_digest());
1667 checkpoint
1668 .verify()
1669 .expect("a freshly built checkpoint verifies");
1670 }
1671
1672 #[test]
1675 fn the_checkpoint_digest_covers_the_header_and_the_bounded_tail() {
1676 let with_tail = KernelCheckpoint::assemble(draft(3, 4, vec![tail_entry(4)])).unwrap();
1677 let without_tail = checkpoint();
1678 assert_eq!(
1679 with_tail.state_digest(),
1680 without_tail.state_digest(),
1681 "the same logical state digests the same either way"
1682 );
1683 assert_ne!(
1684 with_tail.checkpoint_digest(),
1685 without_tail.checkpoint_digest(),
1686 "but the checkpoint digest moves with the tail and the header"
1687 );
1688
1689 let error = tampered(|document| {
1690 document.insert("through_step_seq".to_string(), Value::String("9".into()));
1691 });
1692 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1693 }
1694
1695 #[test]
1696 fn a_checkpoint_round_trips_through_its_bytes() {
1697 let original = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)]))
1698 .expect("a bounded-tail checkpoint assembles");
1699 let decoded =
1700 KernelCheckpoint::from_checkpoint_bytes(original.checkpoint_bytes().as_slice())
1701 .expect("its own bytes decode");
1702 assert_eq!(decoded, original);
1703 assert_eq!(decoded.tail_inputs().len(), 2);
1704 }
1705
1706 #[test]
1707 fn structured_message_body_rejects_removed_body_forms() {
1708 assert!(
1709 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1710 "content_json": "{\\\"Text\\\":\\\"hello\\\"}"
1711 }))
1712 .is_err()
1713 );
1714 assert!(
1715 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1716 "schema_version": 1,
1717 "durable_content": {"blocks": []}
1718 }))
1719 .is_err()
1720 );
1721 }
1722
1723 #[test]
1724 fn structured_message_body_rejects_unknown_fields() {
1725 assert!(
1726 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1727 "durable_content": {"blocks": []},
1728 "unknown": true,
1729 }))
1730 .is_err()
1731 );
1732 }
1733
1734 #[test]
1735 fn removed_durable_content_schema_field_is_not_readable() {
1736 assert!(
1737 serde_json::from_value::<StructuredMessageBody>(serde_json::json!({
1738 "durable_content": {"schema_version": 1, "blocks": []}
1739 }))
1740 .is_err()
1741 );
1742 }
1743
1744 #[test]
1745 fn checkpoint_rejects_durable_tool_result_with_a_second_body_form() {
1746 let mut draft = draft(3, 3, Vec::new());
1747 draft
1748 .logical_state
1749 .context_vm
1750 .messages
1751 .push(StoredMessageState {
1752 partition: MessagePartition::History,
1753 role: "tool".into(),
1754 body: StoredMessageBody::Structured(StructuredMessageBody {
1755 durable_content: Some(crate::types::durable_content::DurableContent::text(
1756 "wrong",
1757 )),
1758 durable_tool_results: vec![
1759 crate::types::durable_content::DurableToolResult::text(
1760 "call-1",
1761 "also wrong",
1762 false,
1763 ),
1764 ],
1765 }),
1766 tool_calls: Vec::new(),
1767 tokens: 0,
1768 });
1769 assert!(matches!(
1770 KernelCheckpoint::assemble(draft),
1771 Err(CheckpointError::Incompatible(_))
1772 ));
1773 }
1774
1775 #[test]
1780 fn a_digest_that_does_not_match_its_bytes_is_corruption() {
1781 for field in ["state_digest", "tail_digest", "checkpoint_digest"] {
1782 let error = tampered(|document| {
1783 document.insert(
1784 field.to_string(),
1785 Value::String(digest("bogus").to_string()),
1786 );
1787 });
1788 assert_eq!(
1789 error.code(),
1790 KernelFaultCode::CheckpointCorrupted,
1791 "{field} must fail closed"
1792 );
1793 assert!(
1794 error.to_string().contains(CHECKPOINT_ERROR_MARKER),
1795 "{field}: every rejection carries the classifier marker"
1796 );
1797 }
1798 }
1799
1800 #[test]
1801 fn a_logical_state_edited_after_the_fact_is_corruption() {
1802 let error = tampered(|document| {
1803 document["logical_state"]["transition"]["lifecycle"] = Value::String("failed".into());
1804 });
1805 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1806 assert!(error.message().contains("logical state hashes to"));
1807 }
1808
1809 #[test]
1810 fn removed_version_fields_are_malformed() {
1811 for field in ["checkpoint_version", "abi_version"] {
1812 let error = tampered(|document| {
1813 document.insert(field.to_string(), Value::from(1));
1814 });
1815 assert_eq!(error.code(), KernelFaultCode::MalformedEnvelope);
1816 }
1817 }
1818
1819 #[test]
1820 fn an_unknown_field_is_refused_rather_than_ignored() {
1821 let error = tampered(|document| {
1822 document.insert("last_step".to_string(), Value::Null);
1824 });
1825 assert_eq!(error.code(), KernelFaultCode::MalformedEnvelope);
1826 }
1827
1828 #[test]
1829 fn a_checkpoint_from_another_operation_or_genesis_is_incompatible() {
1830 let checkpoint = checkpoint();
1831 let other = OperationId::new("op-checkpoint-2").unwrap();
1832
1833 let error = checkpoint
1834 .verify_belongs_to(&other, &digest("genesis"))
1835 .expect_err("another operation's checkpoint is not installable");
1836 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1837 assert!(error.message().contains("belongs to operation"));
1838
1839 let error = checkpoint
1840 .verify_belongs_to(&operation(), &digest("another-genesis"))
1841 .expect_err("a different genesis is a different operation");
1842 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1843 assert!(error.message().contains("binds genesis"));
1844
1845 checkpoint
1846 .verify_belongs_to(&operation(), &digest("genesis"))
1847 .expect("its own operation and genesis are accepted");
1848 }
1849
1850 #[test]
1855 fn a_tail_that_covers_the_range_exactly_is_accepted() {
1856 KernelCheckpoint::assemble(draft(0, 0, Vec::new())).expect("an empty range needs no tail");
1857 KernelCheckpoint::assemble(draft(
1858 2,
1859 5,
1860 vec![tail_entry(3), tail_entry(4), tail_entry(5)],
1861 ))
1862 .expect("(2, 5] is three contiguous inputs");
1863 }
1864
1865 #[test]
1866 fn a_tail_with_a_hole_is_refused() {
1867 let error = KernelCheckpoint::assemble(draft(
1868 2,
1869 5,
1870 vec![tail_entry(3), tail_entry(5), tail_entry(6)],
1871 ))
1872 .expect_err("step 4 is missing");
1873 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1874 assert!(error.message().contains("step 4 was due"), "{error}");
1875 }
1876
1877 #[test]
1878 fn a_tail_with_a_duplicate_is_refused() {
1879 let error = KernelCheckpoint::assemble(draft(
1880 2,
1881 5,
1882 vec![tail_entry(3), tail_entry(3), tail_entry(4)],
1883 ))
1884 .expect_err("step 3 appears twice");
1885 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1886 assert!(error.message().contains("contiguous range"), "{error}");
1887 }
1888
1889 #[test]
1890 fn a_tail_entry_outside_the_range_is_refused() {
1891 let error = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(2), tail_entry(3)]))
1893 .expect_err("step 2 is the base, not part of (2, 4]");
1894 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1895
1896 let error = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(9)]))
1898 .expect_err("step 9 is past the covered head");
1899 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1900 }
1901
1902 #[test]
1903 fn a_tail_whose_length_disagrees_with_the_range_is_refused() {
1904 let error = KernelCheckpoint::assemble(draft(2, 5, vec![tail_entry(3)]))
1905 .expect_err("(2, 5] is three inputs, not one");
1906 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1907 assert!(error.message().contains("bounded tail holds 1"), "{error}");
1908
1909 let error = KernelCheckpoint::assemble(draft(4, 2, Vec::new()))
1910 .expect_err("a base past the covered head is not a range at all");
1911 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1912 }
1913
1914 #[test]
1915 fn a_tail_input_from_another_operation_is_incompatible() {
1916 let mut foreign = tail_entry(3);
1917 foreign.input.operation_id = OperationId::new("op-checkpoint-2").unwrap();
1918 let error = KernelCheckpoint::assemble(draft(2, 3, vec![foreign]))
1919 .expect_err("a tail assembled from two journals is not a checkpoint");
1920 assert_eq!(error.code(), KernelFaultCode::CheckpointIncompatible);
1921 }
1922
1923 #[test]
1926 fn a_tail_edited_in_storage_is_refused_at_decode() {
1927 let original = KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)]))
1928 .expect("assembles");
1929 let mut document: serde_json::Map<String, Value> =
1930 serde_json::from_slice(original.checkpoint_bytes().as_slice()).unwrap();
1931 let tail = document["tail_inputs"].as_array_mut().unwrap();
1932 tail.remove(0);
1933 let bytes = serde_json::to_vec(&document).unwrap();
1934 let error = KernelCheckpoint::from_checkpoint_bytes(&bytes)
1935 .expect_err("a truncated tail no longer covers its range");
1936 assert_eq!(error.code(), KernelFaultCode::CheckpointCorrupted);
1937 }
1938
1939 #[test]
1944 fn a_candidate_carries_the_five_values_of_the_spec_arrow() {
1945 let checkpoint = KernelCheckpoint::assemble(draft(3, 4, vec![tail_entry(4)])).unwrap();
1946 let expected_digest = checkpoint.checkpoint_digest().clone();
1947 let candidate = checkpoint.into_candidate();
1948
1949 assert_eq!(candidate.through_step_seq, WireU64::new(4));
1950 assert_eq!(candidate.covered_head, digest("record-4"));
1951 assert!(
1952 candidate.ack_token.as_str().contains(OPERATION)
1953 && candidate
1954 .ack_token
1955 .as_str()
1956 .contains(expected_digest.as_str()),
1957 "the ack token names the checkpoint it acknowledges: {}",
1958 candidate.ack_token
1959 );
1960
1961 let decoded = candidate.decode().expect("the blob decodes and verifies");
1962 assert_eq!(decoded.checkpoint_digest(), &expected_digest);
1963 assert_eq!(decoded.state_digest(), &candidate.state_digest);
1964 assert_eq!(
1965 candidate.boundary().through_step_seq,
1966 candidate.through_step_seq
1967 );
1968 }
1969
1970 fn fixture_dir() -> PathBuf {
1975 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
1976 }
1977
1978 #[test]
1987 fn bless_checkpoint_rejection_fixtures() {
1988 if std::env::var("BLESS_KERNEL_RECORD_FIXTURES").as_deref() != Ok("1") {
1989 return;
1990 }
1991 let dir = fixture_dir();
1992 for (name, expect, description, mutate) in rejection_cases() {
1993 let mut document: serde_json::Map<String, Value> =
1994 serde_json::from_slice(mutate.0.checkpoint_bytes().as_slice()).unwrap();
1995 (mutate.1)(&mut document);
1996 let fixture = serde_json::json!({
1997 "expect": expect,
1998 "description": description,
1999 "checkpoint": Value::Object(document),
2000 });
2001 let mut text = serde_json::to_string_pretty(&fixture).unwrap();
2002 text.push('\n');
2003 fs::write(dir.join(name), text).unwrap_or_else(|e| panic!("cannot bless {name}: {e}"));
2004 }
2005 }
2006
2007 #[allow(clippy::type_complexity)]
2008 fn rejection_cases() -> Vec<(
2009 &'static str,
2010 &'static str,
2011 &'static str,
2012 (
2013 KernelCheckpoint,
2014 Box<dyn Fn(&mut serde_json::Map<String, Value>)>,
2015 ),
2016 )> {
2017 let full = || checkpoint();
2018 let with_tail =
2019 || KernelCheckpoint::assemble(draft(2, 4, vec![tail_entry(3), tail_entry(4)])).unwrap();
2020 vec![
2021 (
2022 "reject_checkpoint_removed_checkpoint_version.json",
2023 "malformed_envelope",
2024 "A checkpoint carrying the removed checkpoint version field is refused at the \
2025 strict decode boundary.",
2026 (
2027 full(),
2028 Box::new(|d: &mut serde_json::Map<String, Value>| {
2029 d.insert("checkpoint_version".into(), Value::from(99u64));
2030 }) as Box<dyn Fn(&mut serde_json::Map<String, Value>)>,
2031 ),
2032 ),
2033 (
2034 "reject_checkpoint_removed_abi_version.json",
2035 "malformed_envelope",
2036 "A checkpoint carrying the removed ABI version field is refused at the strict \
2037 decode boundary.",
2038 (
2039 full(),
2040 Box::new(|d: &mut serde_json::Map<String, Value>| {
2041 d.insert("abi_version".into(), Value::from(1));
2042 }),
2043 ),
2044 ),
2045 (
2046 "reject_checkpoint_state_digest_mismatch.json",
2047 "checkpoint_corrupted",
2048 "The logical state does not hash to the digest the checkpoint claims (spec 12.1).",
2049 (
2050 full(),
2051 Box::new(|d: &mut serde_json::Map<String, Value>| {
2052 d.insert(
2053 "state_digest".into(),
2054 Value::String(digest("bogus").to_string()),
2055 );
2056 }),
2057 ),
2058 ),
2059 (
2060 "reject_checkpoint_missing_field_checkpoint_digest.json",
2061 "malformed_envelope",
2062 "A structural refusal that names the field: a checkpoint without its own digest is \
2063 not a checkpoint with an unverified digest.",
2064 (
2065 full(),
2066 Box::new(|d: &mut serde_json::Map<String, Value>| {
2067 d.remove("checkpoint_digest");
2068 }),
2069 ),
2070 ),
2071 (
2072 "reject_checkpoint_unknown_field_last_step.json",
2073 "malformed_envelope",
2074 "Spec 12.4 deleted `last_step`; a blob that still carries one is refused \
2075 rather than partially read.",
2076 (
2077 full(),
2078 Box::new(|d: &mut serde_json::Map<String, Value>| {
2079 d.insert("last_step".into(), Value::Null);
2080 }),
2081 ),
2082 ),
2083 (
2084 "reject_checkpoint_base_disagrees_with_covered_head.json",
2085 "checkpoint_corrupted",
2086 "A full-state checkpoint covers no tail, so its base and its covered head name the \
2087 same record; a header that disagrees with itself would hand a restore two \
2088 different chain anchors (spec 12.1, Task 16).",
2089 (
2090 full(),
2091 Box::new(|d: &mut serde_json::Map<String, Value>| {
2092 d.insert(
2093 "base_record_digest".into(),
2094 Value::String(digest("another-record").to_string()),
2095 );
2096 }),
2097 ),
2098 ),
2099 (
2100 "reject_checkpoint_tail_hole.json",
2101 "checkpoint_corrupted",
2102 "The bounded tail must cover (base, through] with no hole (spec 12.1).",
2103 (
2104 with_tail(),
2105 Box::new(|d: &mut serde_json::Map<String, Value>| {
2106 d["tail_inputs"].as_array_mut().unwrap().remove(0);
2107 }),
2108 ),
2109 ),
2110 (
2111 "reject_checkpoint_tail_duplicate.json",
2112 "checkpoint_corrupted",
2113 "The bounded tail must cover (base, through] with no duplicate (spec 12.1).",
2114 (
2115 with_tail(),
2116 Box::new(|d: &mut serde_json::Map<String, Value>| {
2117 let tail = d["tail_inputs"].as_array_mut().unwrap();
2118 tail[1] = tail[0].clone();
2119 }),
2120 ),
2121 ),
2122 (
2123 "reject_checkpoint_tail_foreign_operation.json",
2124 "checkpoint_incompatible",
2125 "A bounded tail assembled from two journals is not a checkpoint (spec 12.1).",
2126 (
2127 with_tail(),
2128 Box::new(|d: &mut serde_json::Map<String, Value>| {
2129 d["tail_inputs"][0]["input"]["operation_id"] =
2130 Value::String("op-checkpoint-2".into());
2131 }),
2132 ),
2133 ),
2134 (
2135 "reject_checkpoint_tail_ends_off_the_covered_head.json",
2136 "checkpoint_corrupted",
2137 "The last bounded-tail entry *is* the covered head; a tail that ends somewhere \
2138 else covers a different prefix than the header claims (spec 12.1, Task 16).",
2139 (
2140 with_tail(),
2141 Box::new(|d: &mut serde_json::Map<String, Value>| {
2142 d["tail_inputs"][1]["record_digest"] =
2143 Value::String(digest("some-other-record").to_string());
2144 }),
2145 ),
2146 ),
2147 ]
2148 }
2149
2150 #[test]
2151 fn checkpoint_rejection_fixtures_fail_closed_with_the_declared_kind() {
2152 let dir = fixture_dir();
2153 let mut names: Vec<String> = fs::read_dir(&dir)
2154 .unwrap_or_else(|e| panic!("failed to read {}: {e}", dir.display()))
2155 .map(|entry| {
2156 entry
2157 .expect("dir entry")
2158 .file_name()
2159 .to_string_lossy()
2160 .to_string()
2161 })
2162 .filter(|name| name.starts_with("reject_checkpoint_") && name.ends_with(".json"))
2163 .collect();
2164 names.sort();
2165 assert!(
2166 names.len() >= 5,
2167 "too few checkpoint rejection fixtures: {names:?}"
2168 );
2169
2170 for name in names {
2171 let raw = fs::read_to_string(dir.join(&name)).expect("fixture reads");
2172 let fixture: Value = serde_json::from_str(&raw).expect("fixture is JSON");
2173 let expected = fixture["expect"]
2174 .as_str()
2175 .expect("every fixture declares `expect`");
2176 let bytes = serde_json::to_vec(&fixture["checkpoint"]).unwrap();
2177 let error = KernelCheckpoint::from_checkpoint_bytes(&bytes)
2178 .expect_err(&format!("{name}: expected a rejection"));
2179 assert_eq!(
2180 error.code().as_str(),
2181 expected,
2182 "{name}: {} (message: {})",
2183 error.code().as_str(),
2184 error.message()
2185 );
2186 for (marker, needle) in [
2189 ("_missing_field_", "missing field"),
2190 ("_unknown_field_", "unknown field"),
2191 ] {
2192 if name.contains(marker) {
2193 assert_eq!(
2194 error.code(),
2195 KernelFaultCode::MalformedEnvelope,
2196 "{name}: a structural refusal is malformed_envelope"
2197 );
2198 assert!(
2199 error.message().contains(needle),
2200 "{name}: the rejection must say which field ({})",
2201 error.message()
2202 );
2203 }
2204 }
2205 }
2206 }
2207}