cellstate_core/
identity.rs

1//! Identity types for CELLSTATE entities
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt;
7use std::hash::Hash;
8use std::str::FromStr;
9use uuid::Uuid;
10
11// ============================================================================
12// ENTITY ID TYPE SYSTEM
13// ============================================================================
14
15/// Trait for type-safe entity IDs.
16///
17/// This trait provides compile-time safety by ensuring entity IDs cannot be
18/// accidentally mixed up. Each entity type has its own strongly-typed ID.
19pub trait EntityIdType:
20    Copy
21    + Clone
22    + Eq
23    + PartialEq
24    + Hash
25    + fmt::Debug
26    + fmt::Display
27    + FromStr
28    + Serialize
29    + serde::de::DeserializeOwned
30    + Send
31    + Sync
32    + 'static
33{
34    /// The name of the entity type (e.g., "tenant", "trajectory").
35    const ENTITY_NAME: &'static str;
36
37    /// Create a new ID from a UUID.
38    fn new(uuid: Uuid) -> Self;
39
40    /// Get the underlying UUID.
41    fn as_uuid(&self) -> Uuid;
42
43    /// Create a nil (all zeros) ID.
44    fn nil() -> Self {
45        Self::new(Uuid::nil())
46    }
47
48    /// Create a new timestamp-sortable UUIDv7 ID.
49    fn now_v7() -> Self {
50        Self::new(Uuid::now_v7())
51    }
52
53    /// Create a new random UUIDv4 ID.
54    fn new_v4() -> Self {
55        Self::new(Uuid::new_v4())
56    }
57}
58
59/// Error type for parsing entity IDs from strings.
60#[derive(Debug, Clone)]
61pub struct EntityIdParseError {
62    pub entity_name: &'static str,
63    pub input: String,
64    pub source: uuid::Error,
65}
66
67impl fmt::Display for EntityIdParseError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(
70            f,
71            "Failed to parse {} ID from '{}': {}",
72            self.entity_name, self.input, self.source
73        )
74    }
75}
76
77impl std::error::Error for EntityIdParseError {
78    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79        Some(&self.source)
80    }
81}
82
83/// Macro to define a type-safe entity ID newtype.
84///
85/// This generates a newtype wrapper around UUID with all the necessary trait
86/// implementations for compile-time type safety.
87macro_rules! define_entity_id {
88    ($name:ident, $entity:literal, $doc:literal) => {
89        #[doc = $doc]
90        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
91        #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
92        pub struct $name(Uuid);
93
94        impl EntityIdType for $name {
95            const ENTITY_NAME: &'static str = $entity;
96
97            fn new(uuid: Uuid) -> Self {
98                Self(uuid)
99            }
100
101            fn as_uuid(&self) -> Uuid {
102                self.0
103            }
104        }
105
106        impl fmt::Debug for $name {
107            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108                write!(f, "{}({})", stringify!($name), self.0)
109            }
110        }
111
112        impl fmt::Display for $name {
113            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114                write!(f, "{}", self.0)
115            }
116        }
117
118        impl FromStr for $name {
119            type Err = EntityIdParseError;
120
121            fn from_str(s: &str) -> Result<Self, Self::Err> {
122                Uuid::from_str(s)
123                    .map(Self::new)
124                    .map_err(|e| EntityIdParseError {
125                        entity_name: Self::ENTITY_NAME,
126                        input: s.to_string(),
127                        source: e,
128                    })
129            }
130        }
131
132        impl Default for $name {
133            fn default() -> Self {
134                Self::nil()
135            }
136        }
137
138        impl Serialize for $name {
139            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
140            where
141                S: serde::Serializer,
142            {
143                // Serialize transparently as UUID string
144                self.0.serialize(serializer)
145            }
146        }
147
148        impl<'de> Deserialize<'de> for $name {
149            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150            where
151                D: serde::Deserializer<'de>,
152            {
153                // Deserialize transparently from UUID
154                Uuid::deserialize(deserializer).map(Self::new)
155            }
156        }
157    };
158}
159
160// ============================================================================
161// ENTITY ID TYPES
162// ============================================================================
163
164define_entity_id!(TenantId, "tenant", "Type-safe ID for tenant entities.");
165define_entity_id!(
166    TrajectoryId,
167    "trajectory",
168    "Type-safe ID for trajectory entities."
169);
170define_entity_id!(ScopeId, "scope", "Type-safe ID for scope entities.");
171define_entity_id!(
172    ArtifactId,
173    "artifact",
174    "Type-safe ID for artifact entities."
175);
176define_entity_id!(NoteId, "note", "Type-safe ID for note entities.");
177define_entity_id!(TurnId, "turn", "Type-safe ID for turn entities.");
178define_entity_id!(AgentId, "agent", "Type-safe ID for agent entities.");
179define_entity_id!(
180    PrincipalId,
181    "principal",
182    "Type-safe ID for principal entities."
183);
184define_entity_id!(
185    WorkingSetId,
186    "working_set",
187    "Type-safe ID for agent working set entities."
188);
189define_entity_id!(EdgeId, "edge", "Type-safe ID for edge entities.");
190define_entity_id!(LockId, "lock", "Type-safe ID for lock entities.");
191define_entity_id!(MessageId, "message", "Type-safe ID for message entities.");
192define_entity_id!(
193    DelegationId,
194    "delegation",
195    "Type-safe ID for delegation entities."
196);
197define_entity_id!(HandoffId, "handoff", "Type-safe ID for handoff entities.");
198define_entity_id!(ApiKeyId, "api_key", "Type-safe ID for API key entities.");
199define_entity_id!(WebhookId, "webhook", "Type-safe ID for webhook entities.");
200define_entity_id!(
201    SummarizationPolicyId,
202    "summarization_policy",
203    "Type-safe ID for summarization policy entities."
204);
205define_entity_id!(
206    SummarizationRequestId,
207    "summarization_request",
208    "Type-safe ID for summarization request entities."
209);
210define_entity_id!(
211    ToolExecutionId,
212    "tool_execution",
213    "Type-safe ID for tool execution entities."
214);
215define_entity_id!(
216    ConflictId,
217    "conflict",
218    "Type-safe ID for conflict entities."
219);
220define_entity_id!(
221    PackConfigId,
222    "pack_config",
223    "Type-safe ID for pack configuration entities."
224);
225define_entity_id!(EventId, "event", "Type-safe ID for event entities.");
226define_entity_id!(FlowId, "flow", "Type-safe ID for flow entities.");
227define_entity_id!(
228    FlowStepId,
229    "flow_step",
230    "Type-safe ID for flow step entities."
231);
232define_entity_id!(
233    SnapshotId,
234    "snapshot",
235    "Type-safe ID for snapshot entities."
236);
237
238// BDI (Belief-Desire-Intention) Agent Primitives (Phase 2)
239define_entity_id!(GoalId, "goal", "Type-safe ID for agent goal entities.");
240define_entity_id!(PlanId, "plan", "Type-safe ID for agent plan entities.");
241define_entity_id!(
242    ActionId,
243    "action",
244    "Type-safe ID for agent action entities."
245);
246define_entity_id!(StepId, "step", "Type-safe ID for plan step entities.");
247define_entity_id!(
248    ObservationId,
249    "observation",
250    "Type-safe ID for agent observation entities."
251);
252define_entity_id!(
253    BeliefId,
254    "belief",
255    "Type-safe ID for agent belief entities."
256);
257define_entity_id!(
258    LearningId,
259    "learning",
260    "Type-safe ID for agent learning entities."
261);
262
263// Team-based access control
264define_entity_id!(TeamId, "team", "Type-safe ID for team entities.");
265
266// Stateful LLM sessions
267define_entity_id!(
268    SessionId,
269    "session",
270    "Type-safe ID for stateful LLM session entities."
271);
272
273// Multi-instance coordination
274define_entity_id!(
275    InstanceId,
276    "instance",
277    "Type-safe ID for API server instance entities."
278);
279
280// ============================================================================
281// OTHER IDENTITY TYPES
282// ============================================================================
283
284/// Timestamp type using UTC timezone.
285pub type Timestamp = DateTime<Utc>;
286
287/// Duration in milliseconds for TTL and timeout values.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
289#[serde(transparent)]
290#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
291pub struct DurationMs(i64);
292
293impl DurationMs {
294    /// Create a new duration from milliseconds.
295    pub const fn new(ms: i64) -> Self {
296        Self(ms)
297    }
298
299    /// Get the raw millisecond value.
300    pub const fn as_millis(&self) -> i64 {
301        self.0
302    }
303
304    /// Convert to a `std::time::Duration`, clamping negative values to zero.
305    pub fn as_duration(&self) -> std::time::Duration {
306        std::time::Duration::from_millis(self.0.max(0) as u64)
307    }
308}
309
310impl fmt::Display for DurationMs {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        write!(f, "{}ms", self.0)
313    }
314}
315
316impl From<i64> for DurationMs {
317    fn from(ms: i64) -> Self {
318        Self(ms)
319    }
320}
321
322impl From<DurationMs> for i64 {
323    fn from(d: DurationMs) -> Self {
324        d.0
325    }
326}
327
328/// SHA-256 content hash for deduplication and integrity verification.
329#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
330pub struct ContentHash([u8; 32]);
331
332impl ContentHash {
333    /// Create a new content hash from raw bytes.
334    pub const fn new(bytes: [u8; 32]) -> Self {
335        Self(bytes)
336    }
337
338    /// Get a reference to the underlying byte array.
339    pub fn as_bytes(&self) -> &[u8; 32] {
340        &self.0
341    }
342
343    /// Get a slice view of the hash bytes.
344    pub fn as_slice(&self) -> &[u8] {
345        &self.0
346    }
347}
348
349impl std::ops::Deref for ContentHash {
350    type Target = [u8; 32];
351
352    fn deref(&self) -> &Self::Target {
353        &self.0
354    }
355}
356
357impl AsRef<[u8]> for ContentHash {
358    fn as_ref(&self) -> &[u8] {
359        &self.0
360    }
361}
362
363impl From<[u8; 32]> for ContentHash {
364    fn from(bytes: [u8; 32]) -> Self {
365        Self(bytes)
366    }
367}
368
369impl From<ContentHash> for [u8; 32] {
370    fn from(hash: ContentHash) -> Self {
371        hash.0
372    }
373}
374
375impl fmt::Debug for ContentHash {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        write!(f, "ContentHash({})", hex::encode(self.0))
378    }
379}
380
381impl fmt::Display for ContentHash {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        write!(f, "{}", hex::encode(self.0))
384    }
385}
386
387impl Serialize for ContentHash {
388    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
389    where
390        S: serde::Serializer,
391    {
392        if serializer.is_human_readable() {
393            hex::encode(self.0).serialize(serializer)
394        } else {
395            self.0.serialize(serializer)
396        }
397    }
398}
399
400impl<'de> Deserialize<'de> for ContentHash {
401    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402    where
403        D: serde::Deserializer<'de>,
404    {
405        if deserializer.is_human_readable() {
406            struct ContentHashVisitor;
407
408            impl<'de> serde::de::Visitor<'de> for ContentHashVisitor {
409                type Value = ContentHash;
410
411                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
412                    formatter.write_str("a hex string or byte array of length 32")
413                }
414
415                fn visit_str<E>(self, v: &str) -> Result<ContentHash, E>
416                where
417                    E: serde::de::Error,
418                {
419                    let v = v.strip_prefix("\\x").unwrap_or(v);
420                    let bytes = hex::decode(v).map_err(serde::de::Error::custom)?;
421                    let arr: [u8; 32] = bytes
422                        .try_into()
423                        .map_err(|_| serde::de::Error::custom("expected 32 bytes"))?;
424                    Ok(ContentHash(arr))
425                }
426
427                fn visit_seq<A>(self, mut seq: A) -> Result<ContentHash, A::Error>
428                where
429                    A: serde::de::SeqAccess<'de>,
430                {
431                    let mut arr = [0u8; 32];
432                    for (i, byte) in arr.iter_mut().enumerate() {
433                        *byte = seq
434                            .next_element()?
435                            .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
436                    }
437                    Ok(ContentHash(arr))
438                }
439            }
440
441            deserializer.deserialize_any(ContentHashVisitor)
442        } else {
443            let bytes = <[u8; 32]>::deserialize(deserializer)?;
444            Ok(ContentHash(bytes))
445        }
446    }
447}
448
449/// Raw binary content for BYTEA storage.
450pub type RawContent = Vec<u8>;
451
452// ============================================================================
453// UTILITY FUNCTIONS
454// ============================================================================
455
456/// Compute SHA-256 hash of content.
457pub fn compute_content_hash(content: &[u8]) -> ContentHash {
458    let mut hasher = Sha256::new();
459    hasher.update(content);
460    let result = hasher.finalize();
461    let mut hash = [0u8; 32];
462    hash.copy_from_slice(&result);
463    ContentHash::new(hash)
464}
465
466// ============================================================================
467// TESTS
468// ============================================================================
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_entity_id_type_safety() {
476        // Different ID types cannot be mixed
477        let tenant_id = TenantId::now_v7();
478        let trajectory_id = TrajectoryId::now_v7();
479
480        // This would not compile if uncommented:
481        // let _: TenantId = trajectory_id;
482
483        assert_ne!(tenant_id.as_uuid(), trajectory_id.as_uuid());
484    }
485
486    #[test]
487    fn test_entity_id_display() {
488        let id = TenantId::new(Uuid::nil());
489        assert_eq!(
490            format!("{:?}", id),
491            "TenantId(00000000-0000-0000-0000-000000000000)"
492        );
493        assert_eq!(format!("{}", id), "00000000-0000-0000-0000-000000000000");
494    }
495
496    #[test]
497    fn test_entity_id_from_str() {
498        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
499        let id: TenantId = uuid_str.parse().expect("valid UUID should parse");
500        assert_eq!(id.to_string(), uuid_str);
501    }
502
503    #[test]
504    fn test_entity_id_parse_error() {
505        let result: Result<TenantId, _> = "invalid".parse();
506        assert!(result.is_err());
507        let err = result.unwrap_err();
508        assert_eq!(err.entity_name, "tenant");
509        assert_eq!(err.input, "invalid");
510    }
511
512    #[test]
513    fn test_entity_id_serde() {
514        let id = TenantId::now_v7();
515        let json = serde_json::to_string(&id).expect("serialization should succeed");
516        // Should serialize as UUID string (not wrapped in object)
517        assert!(json.starts_with('"'));
518        assert!(json.ends_with('"'));
519
520        let deserialized: TenantId =
521            serde_json::from_str(&json).expect("deserialization should succeed");
522        assert_eq!(id, deserialized);
523    }
524
525    #[test]
526    fn test_entity_id_default() {
527        let id = TenantId::default();
528        assert_eq!(id, TenantId::nil());
529    }
530
531    #[test]
532    fn test_all_entity_types() {
533        // Ensure all 15 entity types are defined
534        let _tenant = TenantId::now_v7();
535        let _trajectory = TrajectoryId::now_v7();
536        let _scope = ScopeId::now_v7();
537        let _artifact = ArtifactId::now_v7();
538        let _note = NoteId::now_v7();
539        let _turn = TurnId::now_v7();
540        let _agent = AgentId::now_v7();
541        let _edge = EdgeId::now_v7();
542        let _lock = LockId::now_v7();
543        let _message = MessageId::now_v7();
544        let _delegation = DelegationId::now_v7();
545        let _handoff = HandoffId::now_v7();
546        let _api_key = ApiKeyId::now_v7();
547        let _webhook = WebhookId::now_v7();
548        let _summarization_policy = SummarizationPolicyId::now_v7();
549    }
550
551    #[test]
552    fn test_duration_ms_display() {
553        let d = DurationMs::new(1500);
554        assert_eq!(format!("{}", d), "1500ms");
555    }
556
557    #[test]
558    fn test_duration_ms_from_i64() {
559        let d: DurationMs = 42i64.into();
560        assert_eq!(d.as_millis(), 42);
561    }
562
563    #[test]
564    fn test_duration_ms_serde_roundtrip() {
565        let d = DurationMs::new(3600);
566        let json = serde_json::to_string(&d).expect("serialize");
567        assert_eq!(json, "3600");
568        let restored: DurationMs = serde_json::from_str(&json).expect("deserialize");
569        assert_eq!(d, restored);
570    }
571
572    #[test]
573    fn test_duration_ms_as_duration() {
574        let d = DurationMs::new(1500);
575        assert_eq!(d.as_duration(), std::time::Duration::from_millis(1500));
576    }
577
578    #[test]
579    fn test_duration_ms_negative_clamps() {
580        let d = DurationMs::new(-100);
581        assert_eq!(d.as_duration(), std::time::Duration::from_millis(0));
582    }
583
584    #[test]
585    fn test_content_hash_new_and_access() {
586        let bytes = [42u8; 32];
587        let hash = ContentHash::new(bytes);
588        assert_eq!(*hash.as_bytes(), bytes);
589        assert_eq!(hash.as_slice(), &bytes[..]);
590    }
591
592    #[test]
593    fn test_content_hash_default() {
594        let hash = ContentHash::default();
595        assert_eq!(*hash.as_bytes(), [0u8; 32]);
596    }
597
598    #[test]
599    fn test_content_hash_deref() {
600        let hash = ContentHash::new([1u8; 32]);
601        // Deref to [u8; 32] allows .len()
602        assert_eq!(hash.len(), 32);
603    }
604
605    #[test]
606    fn test_content_hash_from_array() {
607        let bytes = [7u8; 32];
608        let hash: ContentHash = bytes.into();
609        assert_eq!(*hash.as_bytes(), bytes);
610    }
611
612    #[test]
613    fn test_content_hash_into_array() {
614        let hash = ContentHash::new([9u8; 32]);
615        let bytes: [u8; 32] = hash.into();
616        assert_eq!(bytes, [9u8; 32]);
617    }
618
619    #[test]
620    fn test_content_hash_display() {
621        let hash = ContentHash::new([0xab; 32]);
622        let expected = "ab".repeat(32);
623        assert_eq!(format!("{}", hash), expected);
624    }
625
626    #[test]
627    fn test_content_hash_serde_json_roundtrip() {
628        let hash = ContentHash::new([0xde; 32]);
629        let json = serde_json::to_string(&hash).expect("serialize");
630        // Should be a hex string in JSON
631        assert!(json.starts_with('"'));
632        let restored: ContentHash = serde_json::from_str(&json).expect("deserialize");
633        assert_eq!(hash, restored);
634    }
635
636    #[test]
637    fn test_content_hash_deserialize_from_array() {
638        let json_array: Vec<serde_json::Value> = (0u8..32).map(|b| serde_json::json!(b)).collect();
639        let json = serde_json::Value::Array(json_array);
640        let hash: ContentHash = serde_json::from_value(json).expect("deserialize from array");
641        for (i, &b) in hash.as_bytes().iter().enumerate() {
642            assert_eq!(b, i as u8);
643        }
644    }
645
646    #[test]
647    fn test_compute_content_hash_deterministic() {
648        let h1 = compute_content_hash(b"hello");
649        let h2 = compute_content_hash(b"hello");
650        assert_eq!(h1, h2);
651    }
652
653    #[test]
654    fn test_compute_content_hash_different_inputs() {
655        let h1 = compute_content_hash(b"hello");
656        let h2 = compute_content_hash(b"world");
657        assert_ne!(h1, h2);
658    }
659}