Skip to main content

lean_ctx_protocol/
context_checkpoint.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Canonical typed live-state checkpoint domain (`ContextCheckpointV1`).
4//!
5//! A `ContextCheckpointV1` is the portable semantic state needed to *continue*
6//! work: the active task, its progress, the decisions taken, the files in
7//! scope, the pinned policies and packages, and the lineage refs that bind the
8//! checkpoint to its project, workspace, plan, and receipts.  It is explicitly
9//! not a Session Bundle (bounded session transfer), a Handoff Bundle
10//! (point-to-point handoff), a `.ctxpkg` (installable asset), or a Context
11//! Snapshot (signed point-in-time evidence).
12//!
13//! The domain carries **no** machine-local or process-local state: absolute
14//! paths, credentials, API keys, provider cache handles, PIDs, locks, temp
15//! directories, and live-zone payloads are rejected at construction time, not
16//! merely discouraged.  Every bounded string and every collection has an
17//! explicit cap, every identity is validated, and every public constructor is
18//! fallible so that invalid state can never reach a digest or a signature
19//! through an infallible API.
20//!
21//! Projections, merge, migration, signing, and trust evaluation are downstream
22//! slices.  They are represented here only by stable typed seams
23//! ([`ContextCheckpointProjectionV1`], [`ContextCheckpointMergePolicyV1`],
24//! [`ContextCheckpointMigrationV1`], [`ContextCheckpointSignerV1`]) plus the
25//! sealed [`ContextCheckpointSigningPayloadV1`] and
26//! [`ContextCheckpointDigestInputsV1`] carriers those slices consume.
27
28use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
29use serde_json::Value;
30use sha2::{Digest, Sha256};
31
32use crate::{
33    ContextSessionStateV1, DecisionId, PackageId, PlanId, PolicyId, ProfileId, ProjectId,
34    ProtocolReference, ReceiptId, SemanticVersion, SessionIdentityV1, Sha256Digest, SourceId,
35    TaskId, TenantId, UtcTimestamp, ValidationError, WorkspaceId, deserialize_schema_version,
36    validate_milliunit, validate_schema_version,
37};
38
39/// Schema identity of the canonical live-state checkpoint contract.
40pub const CONTEXT_CHECKPOINT_SCHEMA_ID: &str = "leanctx.context-checkpoint/v1";
41
42/// Schema identity of the additive artifact-lineage checkpoint contract.
43///
44/// This is deliberately a distinct wire contract.  `ContextCheckpointV1`
45/// remains byte-for-byte compatible with deployed deny-unknown readers; a
46/// checkpoint carrying authoritative artifact references is a V2 value.
47pub const CONTEXT_CHECKPOINT_V2_SCHEMA_ID: &str = "leanctx.context-checkpoint-live/v2";
48
49/// Domain prefix for the whole-checkpoint content digest.
50pub const CONTEXT_CHECKPOINT_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v1\0";
51
52/// Domain prefix for the checkpoint identity digest.
53pub const CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN: &[u8] =
54    b"leanctx/context-checkpoint-identity/v1\0";
55
56/// Domain prefix for the checkpoint lineage digest.
57pub const CONTEXT_CHECKPOINT_LINEAGE_DIGEST_DOMAIN: &[u8] =
58    b"leanctx/context-checkpoint-lineage/v1\0";
59
60/// Domain prefix for the checkpoint live-state digest.
61pub const CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN: &[u8] =
62    b"leanctx/context-checkpoint-live-state/v1\0";
63
64/// Domain prefix covered by a downstream checkpoint signature.
65pub const CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN: &[u8] = b"leanctx/context-checkpoint-signature/v1\0";
66
67/// Domain prefix for the V2 checkpoint content digest.
68pub const CONTEXT_CHECKPOINT_V2_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v2\0";
69
70/// Domain prefix for the V2 checkpoint lineage digest.
71pub const CONTEXT_CHECKPOINT_V2_LINEAGE_DIGEST_DOMAIN: &[u8] =
72    b"leanctx/context-checkpoint-lineage/v2\0";
73
74/// Domain prefix covered by a V2 checkpoint signature.
75pub const CONTEXT_CHECKPOINT_V2_SIGNATURE_DOMAIN: &[u8] =
76    b"leanctx/context-checkpoint-signature/v2\0";
77
78/// Schema identity of the P6 `.ctxpkg` checkpoint carrier envelope.
79pub const CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID: &str = "leanctx.context-checkpoint/v2";
80
81/// Schema identity of the P6 carrier `logical_state` object.
82pub const CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID: &str = "leanctx.workspace.state/v1";
83
84/// Exact key set of the P6 carrier `logical_state` object, in canonical order.
85///
86/// Downstream projection slices must emit exactly these keys; the constant
87/// exists so the projection cannot silently drift from the carrier verifier.
88pub const CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_KEYS: [&str; 7] = [
89    "entries",
90    "package_lock_digest",
91    "package_pins",
92    "policy",
93    "schema_version",
94    "sources",
95    "workspace_id",
96];
97
98/// Digest domain string used by the P6 carrier for `state_digest`.
99pub const CONTEXT_CHECKPOINT_CARRIER_STATE_DIGEST_DOMAIN: &str = "leanctx.workspace.state.v1";
100
101/// Digest domain string used by the P6 carrier for `envelope_digest`.
102pub const CONTEXT_CHECKPOINT_CARRIER_ENVELOPE_DIGEST_DOMAIN: &str =
103    "leanctx.checkpoint.envelope.v2";
104
105/// Maximum byte length of a bounded checkpoint free-text value.
106pub const MAX_CONTEXT_CHECKPOINT_TEXT_BYTES: usize = 1_024;
107
108/// Maximum encoded checkpoint size accepted before JSON parsing/allocation.
109pub const MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES: usize = 1_048_576;
110
111/// Maximum byte length of a bounded checkpoint identity slug.
112pub const MAX_CONTEXT_CHECKPOINT_SLUG_BYTES: usize = 128;
113
114/// Maximum byte length of an existing protocol identity admitted by a checkpoint.
115pub const MAX_CONTEXT_CHECKPOINT_ID_BYTES: usize = 256;
116
117/// Maximum byte length of a scheme-prefixed checkpoint reference.
118pub const MAX_CONTEXT_CHECKPOINT_REFERENCE_BYTES: usize = 512;
119
120/// Maximum decisions carried by one checkpoint.
121pub const MAX_CONTEXT_CHECKPOINT_DECISIONS: usize = 64;
122
123/// Maximum evidence refs carried by one decision.
124pub const MAX_CONTEXT_CHECKPOINT_DECISION_REFS: usize = 16;
125
126/// Maximum findings carried by one checkpoint.
127pub const MAX_CONTEXT_CHECKPOINT_FINDINGS: usize = 64;
128
129/// Maximum next steps carried by one checkpoint.
130pub const MAX_CONTEXT_CHECKPOINT_NEXT_STEPS: usize = 64;
131
132/// Maximum file entries carried by one checkpoint.
133pub const MAX_CONTEXT_CHECKPOINT_FILES: usize = 256;
134
135/// Maximum policy pins carried by one checkpoint.
136pub const MAX_CONTEXT_CHECKPOINT_POLICY_PINS: usize = 64;
137
138/// Maximum package pins carried by one checkpoint.
139pub const MAX_CONTEXT_CHECKPOINT_PACKAGE_PINS: usize = 128;
140
141/// Maximum entries in any one checkpoint lineage reference list.
142pub const MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS: usize = 64;
143
144/// Maximum step count a checkpoint may report as progress.
145pub const MAX_CONTEXT_CHECKPOINT_STEPS: u32 = 4_096;
146
147/// Strong credential or process-local assignment markers.
148///
149/// These deliberately require value-bearing syntax.  Words such as “secret”,
150/// “lock”, or “provider” are valid task vocabulary and must not make otherwise
151/// portable semantic state impossible to checkpoint.
152const REJECTED_ASSIGNMENTS: [&str; 19] = [
153    "-----begin",
154    "pid=",
155    "process_id=",
156    "lock_id=",
157    "provider_handle=",
158    "cache_handle=",
159    "temp_dir=",
160    "tmpdir=",
161    "flush_timestamp=",
162    "authorization: bearer ",
163    "bearer ",
164    "password=",
165    "password:",
166    "passwd=",
167    "api_key=",
168    "access_key=",
169    "aws_secret=",
170    "client_secret=",
171    "token=",
172];
173
174/// Token prefixes that mark well-known credential shapes.
175///
176/// These are matched per whitespace-delimited token so that ordinary words
177/// which merely contain the prefix (for example `risk-based` for `sk-`) are
178/// not rejected.
179const REJECTED_TOKEN_PREFIXES: [&str; 12] = [
180    "base64:",
181    "ghp_",
182    "gho_",
183    "ghr_",
184    "ghs_",
185    "ghu_",
186    "github_pat_",
187    "hex:",
188    "sk-",
189    "xoxa-",
190    "xoxb-",
191    "xoxp-",
192];
193
194macro_rules! checkpoint_string {
195    ($(#[$meta:meta])* $name:ident, $validate:path) => {
196        $(#[$meta])*
197        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
198        #[serde(transparent)]
199        pub struct $name(String);
200
201        impl $name {
202            /// Construct the value after applying every wire invariant.
203            pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
204                let value = value.into();
205                $validate(&value, stringify!($name))?;
206                Ok(Self(value))
207            }
208
209            /// Borrow the validated wire value.
210            pub fn as_str(&self) -> &str {
211                &self.0
212            }
213
214            /// Consume the value and return its wire representation.
215            pub fn into_inner(self) -> String {
216                self.0
217            }
218        }
219
220        impl AsRef<str> for $name {
221            fn as_ref(&self) -> &str {
222                &self.0
223            }
224        }
225
226        impl std::str::FromStr for $name {
227            type Err = ValidationError;
228
229            fn from_str(value: &str) -> Result<Self, Self::Err> {
230                Self::new(value)
231            }
232        }
233
234        impl TryFrom<&str> for $name {
235            type Error = ValidationError;
236
237            fn try_from(value: &str) -> Result<Self, Self::Error> {
238                Self::new(value)
239            }
240        }
241
242        impl TryFrom<String> for $name {
243            type Error = ValidationError;
244
245            fn try_from(value: String) -> Result<Self, Self::Error> {
246                Self::new(value)
247            }
248        }
249
250        impl From<$name> for String {
251            fn from(value: $name) -> Self {
252                value.0
253            }
254        }
255
256        impl<'de> Deserialize<'de> for $name {
257            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
258            where
259                D: Deserializer<'de>,
260            {
261                let value = String::deserialize(deserializer)?;
262                Self::new(value).map_err(DeError::custom)
263            }
264        }
265    };
266}
267
268macro_rules! validated_deserialize {
269    ($target:ident { $($field:ident : $field_type:ty),+ $(,)? }) => {
270        impl<'de> Deserialize<'de> for $target {
271            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
272            where
273                D: Deserializer<'de>,
274            {
275                #[derive(Deserialize)]
276                #[serde(deny_unknown_fields)]
277                struct Wire {
278                    $($field: $field_type),+
279                }
280
281                let Wire { $($field),+ } = Wire::deserialize(deserializer)?;
282                let value = Self { $($field),+ };
283                value.validate().map_err(DeError::custom)?;
284                Ok(value)
285            }
286        }
287    };
288}
289
290checkpoint_string!(
291    /// Canonical lowercase hyphenated UUID identifying one checkpoint.
292    ContextCheckpointIdV1,
293    validate_canonical_uuid
294);
295
296checkpoint_string!(
297    /// Bounded slug naming the checkpoint branch a device writes to.
298    ContextCheckpointBranchIdV1,
299    validate_checkpoint_slug
300);
301
302checkpoint_string!(
303    /// Bounded slug naming the device that produced a checkpoint.
304    ContextCheckpointDeviceIdV1,
305    validate_checkpoint_slug
306);
307
308checkpoint_string!(
309    /// Bounded single-line free text carried by the live state.
310    ContextCheckpointTextV1,
311    validate_checkpoint_text
312);
313
314mod validation;
315use validation::*;
316mod v1;
317pub use v1::*;
318mod v2;
319pub use v2::*;
320#[cfg(test)]
321mod tests;
322pub(super) use validation::{
323    canonical_bytes_of, digest_with_domain, is_unicode_format_character,
324    reject_machine_local_content, require_capacity, require_sorted_unique, require_unique,
325    validate_checkpoint_identifier, validate_checkpoint_reference,
326};