1use 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
39pub const CONTEXT_CHECKPOINT_SCHEMA_ID: &str = "leanctx.context-checkpoint/v1";
41
42pub const CONTEXT_CHECKPOINT_V2_SCHEMA_ID: &str = "leanctx.context-checkpoint-live/v2";
48
49pub const CONTEXT_CHECKPOINT_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v1\0";
51
52pub const CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN: &[u8] =
54 b"leanctx/context-checkpoint-identity/v1\0";
55
56pub const CONTEXT_CHECKPOINT_LINEAGE_DIGEST_DOMAIN: &[u8] =
58 b"leanctx/context-checkpoint-lineage/v1\0";
59
60pub const CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN: &[u8] =
62 b"leanctx/context-checkpoint-live-state/v1\0";
63
64pub const CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN: &[u8] = b"leanctx/context-checkpoint-signature/v1\0";
66
67pub const CONTEXT_CHECKPOINT_V2_DIGEST_DOMAIN: &[u8] = b"leanctx/context-checkpoint/v2\0";
69
70pub const CONTEXT_CHECKPOINT_V2_LINEAGE_DIGEST_DOMAIN: &[u8] =
72 b"leanctx/context-checkpoint-lineage/v2\0";
73
74pub const CONTEXT_CHECKPOINT_V2_SIGNATURE_DOMAIN: &[u8] =
76 b"leanctx/context-checkpoint-signature/v2\0";
77
78pub const CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID: &str = "leanctx.context-checkpoint/v2";
80
81pub const CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID: &str = "leanctx.workspace.state/v1";
83
84pub 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
98pub const CONTEXT_CHECKPOINT_CARRIER_STATE_DIGEST_DOMAIN: &str = "leanctx.workspace.state.v1";
100
101pub const CONTEXT_CHECKPOINT_CARRIER_ENVELOPE_DIGEST_DOMAIN: &str =
103 "leanctx.checkpoint.envelope.v2";
104
105pub const MAX_CONTEXT_CHECKPOINT_TEXT_BYTES: usize = 1_024;
107
108pub const MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES: usize = 1_048_576;
110
111pub const MAX_CONTEXT_CHECKPOINT_SLUG_BYTES: usize = 128;
113
114pub const MAX_CONTEXT_CHECKPOINT_ID_BYTES: usize = 256;
116
117pub const MAX_CONTEXT_CHECKPOINT_REFERENCE_BYTES: usize = 512;
119
120pub const MAX_CONTEXT_CHECKPOINT_DECISIONS: usize = 64;
122
123pub const MAX_CONTEXT_CHECKPOINT_DECISION_REFS: usize = 16;
125
126pub const MAX_CONTEXT_CHECKPOINT_FINDINGS: usize = 64;
128
129pub const MAX_CONTEXT_CHECKPOINT_NEXT_STEPS: usize = 64;
131
132pub const MAX_CONTEXT_CHECKPOINT_FILES: usize = 256;
134
135pub const MAX_CONTEXT_CHECKPOINT_POLICY_PINS: usize = 64;
137
138pub const MAX_CONTEXT_CHECKPOINT_PACKAGE_PINS: usize = 128;
140
141pub const MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS: usize = 64;
143
144pub const MAX_CONTEXT_CHECKPOINT_STEPS: u32 = 4_096;
146
147const 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
174const 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 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 pub fn as_str(&self) -> &str {
211 &self.0
212 }
213
214 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 ContextCheckpointIdV1,
293 validate_canonical_uuid
294);
295
296checkpoint_string!(
297 ContextCheckpointBranchIdV1,
299 validate_checkpoint_slug
300);
301
302checkpoint_string!(
303 ContextCheckpointDeviceIdV1,
305 validate_checkpoint_slug
306);
307
308checkpoint_string!(
309 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};