1use super::*;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ContextCheckpointV1 {
8 pub schema_version: u32,
10 pub identity: ContextCheckpointIdentityV1,
12 pub lineage: ContextCheckpointLineageV1,
14 pub live_state: ContextCheckpointLiveStateV1,
16 pub carrier: Option<ContextCheckpointCarrierBindingV1>,
18 pub engine_version: SemanticVersion,
20 pub created_at: UtcTimestamp,
22 pub updated_at: UtcTimestamp,
24 pub encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
26}
27
28#[derive(Deserialize)]
29#[serde(deny_unknown_fields)]
30struct ContextCheckpointWireV1 {
31 #[serde(deserialize_with = "deserialize_schema_version")]
32 schema_version: u32,
33 identity: ContextCheckpointIdentityV1,
34 lineage: ContextCheckpointLineageV1,
35 live_state: ContextCheckpointLiveStateV1,
36 carrier: Option<ContextCheckpointCarrierBindingV1>,
37 engine_version: SemanticVersion,
38 created_at: UtcTimestamp,
39 #[serde(default)]
40 updated_at: LegacyRequiredField<UtcTimestamp>,
41 #[serde(default)]
42 encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
43}
44
45#[derive(Default)]
46enum LegacyRequiredField<T> {
47 #[default]
48 Missing,
49 Present(T),
50}
51
52impl<'de, T> Deserialize<'de> for LegacyRequiredField<T>
53where
54 T: Deserialize<'de>,
55{
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: Deserializer<'de>,
59 {
60 T::deserialize(deserializer).map(Self::Present)
61 }
62}
63
64impl<'de> Deserialize<'de> for ContextCheckpointV1 {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: Deserializer<'de>,
68 {
69 let wire = ContextCheckpointWireV1::deserialize(deserializer)?;
70 let checkpoint = Self {
71 schema_version: wire.schema_version,
72 identity: wire.identity,
73 lineage: wire.lineage,
74 live_state: wire.live_state,
75 carrier: wire.carrier,
76 engine_version: wire.engine_version,
77 updated_at: match wire.updated_at {
78 LegacyRequiredField::Missing => wire.created_at.clone(),
79 LegacyRequiredField::Present(updated_at) => updated_at,
80 },
81 created_at: wire.created_at,
82 encryption_metadata: wire.encryption_metadata,
83 };
84 checkpoint.validate().map_err(D::Error::custom)?;
85 Ok(checkpoint)
86 }
87}
88
89impl ContextCheckpointV1 {
90 pub const SCHEMA_VERSION: u32 = 1;
92
93 pub const SCHEMA_ID: &'static str = CONTEXT_CHECKPOINT_SCHEMA_ID;
95
96 pub fn try_new(
102 identity: ContextCheckpointIdentityV1,
103 lineage: ContextCheckpointLineageV1,
104 live_state: ContextCheckpointLiveStateV1,
105 carrier: Option<ContextCheckpointCarrierBindingV1>,
106 engine_version: SemanticVersion,
107 created_at: UtcTimestamp,
108 ) -> Result<Self, ValidationError> {
109 let checkpoint = Self {
110 schema_version: Self::SCHEMA_VERSION,
111 identity,
112 lineage,
113 live_state,
114 carrier,
115 engine_version,
116 updated_at: created_at.clone(),
117 created_at,
118 encryption_metadata: None,
119 };
120 checkpoint.validate()?;
121 Ok(checkpoint)
122 }
123
124 pub fn with_optional_metadata(
126 mut self,
127 encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
128 ) -> Result<Self, ValidationError> {
129 self.encryption_metadata = encryption_metadata;
130 self.validate()?;
131 Ok(self)
132 }
133
134 pub fn with_updated_at(mut self, updated_at: UtcTimestamp) -> Result<Self, ValidationError> {
136 self.updated_at = updated_at;
137 self.validate()?;
138 Ok(self)
139 }
140
141 pub fn with_portable_state(
143 mut self,
144 session_state: Option<ContextCheckpointSessionStateV1>,
145 learning_state: Option<ContextCheckpointLearningStateV1>,
146 encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
147 ) -> Result<Self, ValidationError> {
148 self.live_state.session_state = session_state;
149 self.live_state.learning_state = learning_state;
150 self.encryption_metadata = encryption_metadata;
151 self.validate()?;
152 Ok(self)
153 }
154
155 pub fn validate(&self) -> Result<(), ValidationError> {
157 validate_schema_version(self.schema_version)?;
158 self.identity.validate()?;
159 self.lineage.validate()?;
160 self.live_state.validate()?;
161 if self.live_state.task.task_id != self.lineage.task_id {
162 return Err(ValidationError::new(
163 "live_state task_id must equal lineage task_id",
164 ));
165 }
166 if self.live_state.task.plan_id != self.lineage.plan_id {
167 return Err(ValidationError::new(
168 "live_state plan_id must equal lineage plan_id",
169 ));
170 }
171 if let Some(carrier) = &self.carrier {
172 carrier.validate()?;
173 if carrier.workspace_id != self.lineage.workspace_id {
174 return Err(ValidationError::new(
175 "carrier workspace_id must equal lineage workspace_id",
176 ));
177 }
178 }
179 if let Some(encryption_metadata) = &self.encryption_metadata {
180 encryption_metadata.validate()?;
181 }
182 if let Some(session) = &self.live_state.session_state {
183 if session.identity.tenant_id.as_ref() != Some(&self.lineage.tenant_id)
184 || session.identity.project_id != self.lineage.project_id
185 || session.identity.workspace_id.as_ref() != Some(&self.lineage.workspace_id)
186 || session.identity.task_id != self.lineage.task_id
187 {
188 return Err(ValidationError::new(
189 "session identity must match checkpoint tenant, project, workspace, and task",
190 ));
191 }
192 if self.lineage.plan_id.is_none() {
193 return Err(ValidationError::new(
194 "session checkpoints require an explicit plan_id",
195 ));
196 }
197 if session.state.active_plan_id != self.lineage.plan_id {
198 return Err(ValidationError::new(
199 "session active_plan_id must equal checkpoint lineage plan_id",
200 ));
201 }
202 }
203 if self.updated_at < self.created_at {
204 return Err(ValidationError::new(
205 "updated_at must not precede created_at",
206 ));
207 }
208 Ok(())
209 }
210
211 pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
213 self.validate()?;
214 canonical_bytes_of(self, "checkpoint")
215 }
216
217 pub fn canonical_json(&self) -> Result<String, ValidationError> {
219 String::from_utf8(self.canonical_bytes()?)
220 .map_err(|error| ValidationError::new(format!("checkpoint canonical UTF-8: {error}")))
221 }
222
223 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
225 if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
226 return Err(ValidationError::new(format!(
227 "checkpoint exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
228 )));
229 }
230 let checkpoint = serde_json::from_slice::<Self>(bytes)
231 .map_err(|error| ValidationError::new(format!("decode checkpoint: {error}")))?;
232 if checkpoint.canonical_bytes()? != bytes {
233 return Err(ValidationError::new(
234 "checkpoint JSON is not canonical UTF-8, compactness or key order",
235 ));
236 }
237 Ok(checkpoint)
238 }
239
240 pub fn from_legacy_v1_canonical_bytes(
246 bytes: &[u8],
247 authenticated_tenant_id: TenantId,
248 ) -> Result<Self, ValidationError> {
249 if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
250 return Err(ValidationError::new(format!(
251 "checkpoint exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
252 )));
253 }
254 let mut value: Value = serde_json::from_slice(bytes)
255 .map_err(|error| ValidationError::new(format!("decode legacy checkpoint: {error}")))?;
256 let canonical_legacy = serde_json::to_vec(&value).map_err(|error| {
257 ValidationError::new(format!("canonicalize legacy checkpoint: {error}"))
258 })?;
259 if canonical_legacy != bytes {
260 return Err(ValidationError::new(
261 "legacy checkpoint JSON is not canonical UTF-8, compactness or key order",
262 ));
263 }
264 let lineage = value
265 .get_mut("lineage")
266 .and_then(Value::as_object_mut)
267 .ok_or_else(|| ValidationError::new("legacy checkpoint lineage must be an object"))?;
268 if lineage.contains_key("tenant_id") {
269 return Err(ValidationError::new(
270 "legacy migration rejects checkpoints that already carry tenant_id",
271 ));
272 }
273 lineage.insert(
274 "tenant_id".to_owned(),
275 Value::String(authenticated_tenant_id.as_str().to_owned()),
276 );
277 let checkpoint = serde_json::from_value::<Self>(value)
278 .map_err(|error| ValidationError::new(format!("migrate legacy checkpoint: {error}")))?;
279 checkpoint.validate()?;
280 Ok(checkpoint)
281 }
282
283 pub fn digest(&self) -> Result<Sha256Digest, ValidationError> {
285 let bytes = self.canonical_bytes()?;
286 digest_with_domain(CONTEXT_CHECKPOINT_DIGEST_DOMAIN, &bytes)
287 }
288
289 pub fn digest_inputs(&self) -> Result<ContextCheckpointDigestInputsV1, ValidationError> {
291 self.validate()?;
292 Ok(ContextCheckpointDigestInputsV1 {
293 identity_digest: digest_with_domain(
294 CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN,
295 &canonical_bytes_of(&self.identity, "checkpoint identity")?,
296 )?,
297 lineage_digest: digest_with_domain(
298 CONTEXT_CHECKPOINT_LINEAGE_DIGEST_DOMAIN,
299 &canonical_bytes_of(&self.lineage, "checkpoint lineage")?,
300 )?,
301 live_state_digest: digest_with_domain(
302 CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN,
303 &canonical_bytes_of(&self.live_state, "checkpoint live state")?,
304 )?,
305 checkpoint_digest: self.digest()?,
306 })
307 }
308
309 pub fn signing_payload(&self) -> Result<ContextCheckpointSigningPayloadV1, ValidationError> {
311 let canonical = self.canonical_bytes()?;
312 let mut bytes =
313 Vec::with_capacity(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN.len() + canonical.len());
314 bytes.extend_from_slice(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN);
315 bytes.extend_from_slice(&canonical);
316 let digest = digest_with_domain(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN, &canonical)?;
317 Ok(ContextCheckpointSigningPayloadV1 { bytes, digest })
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct ContextCheckpointV2 {
328 pub schema_version: u32,
329 pub identity: ContextCheckpointIdentityV1,
330 pub lineage: ContextCheckpointLineageV2,
331 pub live_state: ContextCheckpointLiveStateV1,
332 pub carrier: Option<ContextCheckpointCarrierBindingV1>,
333 pub engine_version: SemanticVersion,
334 pub created_at: UtcTimestamp,
335 pub updated_at: UtcTimestamp,
336 pub encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
337}
338
339#[derive(Deserialize)]
340#[serde(deny_unknown_fields)]
341struct ContextCheckpointWireV2 {
342 schema_version: u32,
343 identity: ContextCheckpointIdentityV1,
344 lineage: ContextCheckpointLineageV2,
345 live_state: ContextCheckpointLiveStateV1,
346 carrier: Option<ContextCheckpointCarrierBindingV1>,
347 engine_version: SemanticVersion,
348 created_at: UtcTimestamp,
349 updated_at: UtcTimestamp,
350 #[serde(default)]
351 encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
352}
353
354impl<'de> Deserialize<'de> for ContextCheckpointV2 {
355 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356 where
357 D: Deserializer<'de>,
358 {
359 let wire = ContextCheckpointWireV2::deserialize(deserializer)?;
360 let checkpoint = Self {
361 schema_version: wire.schema_version,
362 identity: wire.identity,
363 lineage: wire.lineage,
364 live_state: wire.live_state,
365 carrier: wire.carrier,
366 engine_version: wire.engine_version,
367 created_at: wire.created_at,
368 updated_at: wire.updated_at,
369 encryption_metadata: wire.encryption_metadata,
370 };
371 checkpoint.validate().map_err(D::Error::custom)?;
372 Ok(checkpoint)
373 }
374}
375
376impl ContextCheckpointV2 {
377 pub const SCHEMA_VERSION: u32 = 2;
379
380 pub const SCHEMA_ID: &'static str = CONTEXT_CHECKPOINT_V2_SCHEMA_ID;
382
383 pub fn from_v1_unverified(
390 checkpoint: ContextCheckpointV1,
391 artifact_lineage: ContextCheckpointArtifactLineageV1,
392 ) -> Result<Self, ValidationError> {
393 checkpoint.validate()?;
394 artifact_lineage.validate()?;
395 let migrated = Self {
396 schema_version: Self::SCHEMA_VERSION,
397 identity: checkpoint.identity,
398 lineage: ContextCheckpointLineageV2 {
399 project_id: checkpoint.lineage.project_id,
400 workspace_id: checkpoint.lineage.workspace_id,
401 tenant_id: checkpoint.lineage.tenant_id,
402 task_id: checkpoint.lineage.task_id,
403 plan_id: checkpoint.lineage.plan_id,
404 receipt_ids: checkpoint.lineage.receipt_ids,
405 artifact_lineage,
406 context_ir_digest: checkpoint.lineage.context_ir_digest,
407 hosted_index_digest: checkpoint.lineage.hosted_index_digest,
408 evidence_refs: checkpoint.lineage.evidence_refs,
409 knowledge_refs: checkpoint.lineage.knowledge_refs,
410 gotcha_refs: checkpoint.lineage.gotcha_refs,
411 snapshot_refs: checkpoint.lineage.snapshot_refs,
412 },
413 live_state: checkpoint.live_state,
414 carrier: checkpoint.carrier,
415 engine_version: checkpoint.engine_version,
416 created_at: checkpoint.created_at,
417 updated_at: checkpoint.updated_at,
418 encryption_metadata: checkpoint.encryption_metadata,
419 };
420 migrated.validate()?;
421 Ok(migrated)
422 }
423
424 pub fn validate(&self) -> Result<(), ValidationError> {
426 if self.schema_version != Self::SCHEMA_VERSION {
427 return Err(ValidationError::new(format!(
428 "unsupported checkpoint V2 schema_version {}; expected {}",
429 self.schema_version,
430 Self::SCHEMA_VERSION
431 )));
432 }
433 self.lineage.validate()?;
434 self.legacy_checkpoint()?.validate()
435 }
436
437 fn legacy_checkpoint(&self) -> Result<ContextCheckpointV1, ValidationError> {
438 Ok(ContextCheckpointV1 {
439 schema_version: ContextCheckpointV1::SCHEMA_VERSION,
440 identity: self.identity.clone(),
441 lineage: self.lineage.legacy(),
442 live_state: self.live_state.clone(),
443 carrier: self.carrier.clone(),
444 engine_version: self.engine_version.clone(),
445 created_at: self.created_at.clone(),
446 updated_at: self.updated_at.clone(),
447 encryption_metadata: self.encryption_metadata.clone(),
448 })
449 }
450
451 pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
453 self.validate()?;
454 canonical_bytes_of(self, "checkpoint V2")
455 }
456
457 pub fn canonical_json(&self) -> Result<String, ValidationError> {
459 String::from_utf8(self.canonical_bytes()?)
460 .map_err(|error| ValidationError::new(format!("checkpoint V2 UTF-8: {error}")))
461 }
462
463 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
465 if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
466 return Err(ValidationError::new(format!(
467 "checkpoint V2 exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
468 )));
469 }
470 let checkpoint = serde_json::from_slice::<Self>(bytes)
471 .map_err(|error| ValidationError::new(format!("decode checkpoint V2: {error}")))?;
472 if checkpoint.canonical_bytes()? != bytes {
473 return Err(ValidationError::new(
474 "checkpoint V2 JSON is not canonical UTF-8, compactness or key order",
475 ));
476 }
477 Ok(checkpoint)
478 }
479
480 pub fn digest(&self) -> Result<Sha256Digest, ValidationError> {
482 digest_with_domain(
483 CONTEXT_CHECKPOINT_V2_DIGEST_DOMAIN,
484 &self.canonical_bytes()?,
485 )
486 }
487
488 pub fn digest_inputs(&self) -> Result<ContextCheckpointDigestInputsV1, ValidationError> {
490 self.validate()?;
491 Ok(ContextCheckpointDigestInputsV1 {
492 identity_digest: digest_with_domain(
493 CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN,
494 &canonical_bytes_of(&self.identity, "checkpoint V2 identity")?,
495 )?,
496 lineage_digest: digest_with_domain(
497 CONTEXT_CHECKPOINT_V2_LINEAGE_DIGEST_DOMAIN,
498 &canonical_bytes_of(&self.lineage, "checkpoint V2 lineage")?,
499 )?,
500 live_state_digest: digest_with_domain(
501 CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN,
502 &canonical_bytes_of(&self.live_state, "checkpoint V2 live state")?,
503 )?,
504 checkpoint_digest: self.digest()?,
505 })
506 }
507}
508
509impl Serialize for ContextCheckpointV2 {
510 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
511 where
512 S: serde::Serializer,
513 {
514 self.validate().map_err(serde::ser::Error::custom)?;
515 #[derive(Serialize)]
516 struct Wire<'a> {
517 schema_version: u32,
518 identity: &'a ContextCheckpointIdentityV1,
519 lineage: &'a ContextCheckpointLineageV2,
520 live_state: &'a ContextCheckpointLiveStateV1,
521 carrier: &'a Option<ContextCheckpointCarrierBindingV1>,
522 engine_version: &'a SemanticVersion,
523 created_at: &'a UtcTimestamp,
524 updated_at: &'a UtcTimestamp,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 encryption_metadata: &'a Option<ContextCheckpointEncryptionMetadataV1>,
527 }
528 Wire {
529 schema_version: self.schema_version,
530 identity: &self.identity,
531 lineage: &self.lineage,
532 live_state: &self.live_state,
533 carrier: &self.carrier,
534 engine_version: &self.engine_version,
535 created_at: &self.created_at,
536 updated_at: &self.updated_at,
537 encryption_metadata: &self.encryption_metadata,
538 }
539 .serialize(serializer)
540 }
541}
542
543impl Serialize for ContextCheckpointV1 {
544 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
545 where
546 S: serde::Serializer,
547 {
548 self.validate().map_err(serde::ser::Error::custom)?;
549 #[derive(Serialize)]
550 struct Wire<'a> {
551 schema_version: u32,
552 identity: &'a ContextCheckpointIdentityV1,
553 lineage: &'a ContextCheckpointLineageV1,
554 live_state: &'a ContextCheckpointLiveStateV1,
555 carrier: &'a Option<ContextCheckpointCarrierBindingV1>,
556 engine_version: &'a SemanticVersion,
557 created_at: &'a UtcTimestamp,
558 updated_at: &'a UtcTimestamp,
559 #[serde(skip_serializing_if = "Option::is_none")]
560 encryption_metadata: &'a Option<ContextCheckpointEncryptionMetadataV1>,
561 }
562 Wire {
563 schema_version: self.schema_version,
564 identity: &self.identity,
565 lineage: &self.lineage,
566 live_state: &self.live_state,
567 carrier: &self.carrier,
568 engine_version: &self.engine_version,
569 created_at: &self.created_at,
570 updated_at: &self.updated_at,
571 encryption_metadata: &self.encryption_metadata,
572 }
573 .serialize(serializer)
574 }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct ContextCheckpointDigestInputsV1 {
583 identity_digest: Sha256Digest,
584 lineage_digest: Sha256Digest,
585 live_state_digest: Sha256Digest,
586 checkpoint_digest: Sha256Digest,
587}
588
589impl ContextCheckpointDigestInputsV1 {
590 pub fn identity_digest(&self) -> &Sha256Digest {
592 &self.identity_digest
593 }
594
595 pub fn lineage_digest(&self) -> &Sha256Digest {
597 &self.lineage_digest
598 }
599
600 pub fn live_state_digest(&self) -> &Sha256Digest {
602 &self.live_state_digest
603 }
604
605 pub fn checkpoint_digest(&self) -> &Sha256Digest {
607 &self.checkpoint_digest
608 }
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
617pub struct ContextCheckpointSigningPayloadV1 {
618 bytes: Vec<u8>,
619 digest: Sha256Digest,
620}
621
622impl ContextCheckpointSigningPayloadV1 {
623 pub fn bytes(&self) -> &[u8] {
625 &self.bytes
626 }
627
628 pub fn digest(&self) -> &Sha256Digest {
630 &self.digest
631 }
632}
633
634pub trait ContextCheckpointProjectionV1 {
636 type Projection;
638 type Error;
640
641 fn project(&self, checkpoint: &ContextCheckpointV1) -> Result<Self::Projection, Self::Error>;
643}
644
645pub trait ContextCheckpointMergePolicyV1 {
647 type Error;
649
650 fn merge(
652 &self,
653 ancestor: &ContextCheckpointV1,
654 left: &ContextCheckpointV1,
655 right: &ContextCheckpointV1,
656 ) -> Result<ContextCheckpointV1, Self::Error>;
657}
658
659pub trait ContextCheckpointMigrationV1 {
661 type Error;
663
664 fn migrate(&self, bytes: &[u8]) -> Result<ContextCheckpointV1, Self::Error>;
666}
667
668pub trait ContextCheckpointSignerV1 {
670 type Signature;
672 type Error;
674
675 fn sign(
677 &self,
678 payload: &ContextCheckpointSigningPayloadV1,
679 ) -> Result<Self::Signature, Self::Error>;
680}