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// Pack deployment
281define_entity_id!(
282    DeploymentId,
283    "deployment",
284    "Type-safe ID for pack deployment entities."
285);
286
287// ============================================================================
288// OTHER IDENTITY TYPES
289// ============================================================================
290
291/// Timestamp type using UTC timezone.
292pub type Timestamp = DateTime<Utc>;
293
294/// Duration in milliseconds for TTL and timeout values.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
296#[serde(transparent)]
297#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
298pub struct DurationMs(i64);
299
300impl DurationMs {
301    /// Create a new duration from milliseconds.
302    pub const fn new(ms: i64) -> Self {
303        Self(ms)
304    }
305
306    /// Get the raw millisecond value.
307    pub const fn as_millis(&self) -> i64 {
308        self.0
309    }
310
311    /// Convert to a `std::time::Duration`, clamping negative values to zero.
312    pub fn as_duration(&self) -> std::time::Duration {
313        std::time::Duration::from_millis(self.0.max(0) as u64)
314    }
315}
316
317impl fmt::Display for DurationMs {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}ms", self.0)
320    }
321}
322
323impl From<i64> for DurationMs {
324    fn from(ms: i64) -> Self {
325        Self(ms)
326    }
327}
328
329impl From<DurationMs> for i64 {
330    fn from(d: DurationMs) -> Self {
331        d.0
332    }
333}
334
335/// SHA-256 content hash for deduplication and integrity verification.
336#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
337pub struct ContentHash([u8; 32]);
338
339impl ContentHash {
340    /// Create a new content hash from raw bytes.
341    pub const fn new(bytes: [u8; 32]) -> Self {
342        Self(bytes)
343    }
344
345    /// Get a reference to the underlying byte array.
346    pub fn as_bytes(&self) -> &[u8; 32] {
347        &self.0
348    }
349
350    /// Get a slice view of the hash bytes.
351    pub fn as_slice(&self) -> &[u8] {
352        &self.0
353    }
354}
355
356impl std::ops::Deref for ContentHash {
357    type Target = [u8; 32];
358
359    fn deref(&self) -> &Self::Target {
360        &self.0
361    }
362}
363
364impl AsRef<[u8]> for ContentHash {
365    fn as_ref(&self) -> &[u8] {
366        &self.0
367    }
368}
369
370impl From<[u8; 32]> for ContentHash {
371    fn from(bytes: [u8; 32]) -> Self {
372        Self(bytes)
373    }
374}
375
376impl From<ContentHash> for [u8; 32] {
377    fn from(hash: ContentHash) -> Self {
378        hash.0
379    }
380}
381
382impl fmt::Debug for ContentHash {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        write!(f, "ContentHash({})", hex::encode(self.0))
385    }
386}
387
388impl fmt::Display for ContentHash {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        write!(f, "{}", hex::encode(self.0))
391    }
392}
393
394impl Serialize for ContentHash {
395    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
396    where
397        S: serde::Serializer,
398    {
399        if serializer.is_human_readable() {
400            hex::encode(self.0).serialize(serializer)
401        } else {
402            self.0.serialize(serializer)
403        }
404    }
405}
406
407impl<'de> Deserialize<'de> for ContentHash {
408    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
409    where
410        D: serde::Deserializer<'de>,
411    {
412        if deserializer.is_human_readable() {
413            struct ContentHashVisitor;
414
415            impl<'de> serde::de::Visitor<'de> for ContentHashVisitor {
416                type Value = ContentHash;
417
418                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
419                    formatter.write_str("a hex string or byte array of length 32")
420                }
421
422                fn visit_str<E>(self, v: &str) -> Result<ContentHash, E>
423                where
424                    E: serde::de::Error,
425                {
426                    let v = v.strip_prefix("\\x").unwrap_or(v);
427                    let bytes = hex::decode(v).map_err(serde::de::Error::custom)?;
428                    let arr: [u8; 32] = bytes
429                        .try_into()
430                        .map_err(|_| serde::de::Error::custom("expected 32 bytes"))?;
431                    Ok(ContentHash(arr))
432                }
433
434                fn visit_seq<A>(self, mut seq: A) -> Result<ContentHash, A::Error>
435                where
436                    A: serde::de::SeqAccess<'de>,
437                {
438                    let mut arr = [0u8; 32];
439                    for (i, byte) in arr.iter_mut().enumerate() {
440                        *byte = seq
441                            .next_element()?
442                            .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
443                    }
444                    Ok(ContentHash(arr))
445                }
446            }
447
448            deserializer.deserialize_any(ContentHashVisitor)
449        } else {
450            let bytes = <[u8; 32]>::deserialize(deserializer)?;
451            Ok(ContentHash(bytes))
452        }
453    }
454}
455
456/// Raw binary content for BYTEA storage.
457pub type RawContent = Vec<u8>;
458
459// ============================================================================
460// UTILITY FUNCTIONS
461// ============================================================================
462
463/// Compute SHA-256 hash of content.
464pub fn compute_content_hash(content: &[u8]) -> ContentHash {
465    let mut hasher = Sha256::new();
466    hasher.update(content);
467    let result = hasher.finalize();
468    let mut hash = [0u8; 32];
469    hash.copy_from_slice(&result);
470    ContentHash::new(hash)
471}
472
473// ============================================================================
474// TESTS
475// ============================================================================
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn test_entity_id_type_safety() {
483        // Different ID types cannot be mixed
484        let tenant_id = TenantId::now_v7();
485        let trajectory_id = TrajectoryId::now_v7();
486
487        // This would not compile if uncommented:
488        // let _: TenantId = trajectory_id;
489
490        assert_ne!(tenant_id.as_uuid(), trajectory_id.as_uuid());
491    }
492
493    #[test]
494    fn test_entity_id_display() {
495        let id = TenantId::new(Uuid::nil());
496        assert_eq!(
497            format!("{:?}", id),
498            "TenantId(00000000-0000-0000-0000-000000000000)"
499        );
500        assert_eq!(format!("{}", id), "00000000-0000-0000-0000-000000000000");
501    }
502
503    #[test]
504    fn test_entity_id_from_str() {
505        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
506        let id: TenantId = uuid_str.parse().expect("valid UUID should parse");
507        assert_eq!(id.to_string(), uuid_str);
508    }
509
510    #[test]
511    fn test_entity_id_parse_error() {
512        let result: Result<TenantId, _> = "invalid".parse();
513        assert!(result.is_err());
514        let err = result.unwrap_err();
515        assert_eq!(err.entity_name, "tenant");
516        assert_eq!(err.input, "invalid");
517    }
518
519    #[test]
520    fn test_entity_id_serde() {
521        let id = TenantId::now_v7();
522        let json = serde_json::to_string(&id).expect("serialization should succeed");
523        // Should serialize as UUID string (not wrapped in object)
524        assert!(json.starts_with('"'));
525        assert!(json.ends_with('"'));
526
527        let deserialized: TenantId =
528            serde_json::from_str(&json).expect("deserialization should succeed");
529        assert_eq!(id, deserialized);
530    }
531
532    #[test]
533    fn test_entity_id_default() {
534        let id = TenantId::default();
535        assert_eq!(id, TenantId::nil());
536    }
537
538    #[test]
539    fn test_all_entity_types() {
540        // Ensure all 15 entity types are defined
541        let _tenant = TenantId::now_v7();
542        let _trajectory = TrajectoryId::now_v7();
543        let _scope = ScopeId::now_v7();
544        let _artifact = ArtifactId::now_v7();
545        let _note = NoteId::now_v7();
546        let _turn = TurnId::now_v7();
547        let _agent = AgentId::now_v7();
548        let _edge = EdgeId::now_v7();
549        let _lock = LockId::now_v7();
550        let _message = MessageId::now_v7();
551        let _delegation = DelegationId::now_v7();
552        let _handoff = HandoffId::now_v7();
553        let _api_key = ApiKeyId::now_v7();
554        let _webhook = WebhookId::now_v7();
555        let _summarization_policy = SummarizationPolicyId::now_v7();
556    }
557
558    #[test]
559    fn test_duration_ms_display() {
560        let d = DurationMs::new(1500);
561        assert_eq!(format!("{}", d), "1500ms");
562    }
563
564    #[test]
565    fn test_duration_ms_from_i64() {
566        let d: DurationMs = 42i64.into();
567        assert_eq!(d.as_millis(), 42);
568    }
569
570    #[test]
571    fn test_duration_ms_serde_roundtrip() {
572        let d = DurationMs::new(3600);
573        let json = serde_json::to_string(&d).expect("serialize");
574        assert_eq!(json, "3600");
575        let restored: DurationMs = serde_json::from_str(&json).expect("deserialize");
576        assert_eq!(d, restored);
577    }
578
579    #[test]
580    fn test_duration_ms_as_duration() {
581        let d = DurationMs::new(1500);
582        assert_eq!(d.as_duration(), std::time::Duration::from_millis(1500));
583    }
584
585    #[test]
586    fn test_duration_ms_negative_clamps() {
587        let d = DurationMs::new(-100);
588        assert_eq!(d.as_duration(), std::time::Duration::from_millis(0));
589    }
590
591    #[test]
592    fn test_content_hash_new_and_access() {
593        let bytes = [42u8; 32];
594        let hash = ContentHash::new(bytes);
595        assert_eq!(*hash.as_bytes(), bytes);
596        assert_eq!(hash.as_slice(), &bytes[..]);
597    }
598
599    #[test]
600    fn test_content_hash_default() {
601        let hash = ContentHash::default();
602        assert_eq!(*hash.as_bytes(), [0u8; 32]);
603    }
604
605    #[test]
606    fn test_content_hash_deref() {
607        let hash = ContentHash::new([1u8; 32]);
608        // Deref to [u8; 32] allows .len()
609        assert_eq!(hash.len(), 32);
610    }
611
612    #[test]
613    fn test_content_hash_from_array() {
614        let bytes = [7u8; 32];
615        let hash: ContentHash = bytes.into();
616        assert_eq!(*hash.as_bytes(), bytes);
617    }
618
619    #[test]
620    fn test_content_hash_into_array() {
621        let hash = ContentHash::new([9u8; 32]);
622        let bytes: [u8; 32] = hash.into();
623        assert_eq!(bytes, [9u8; 32]);
624    }
625
626    #[test]
627    fn test_content_hash_display() {
628        let hash = ContentHash::new([0xab; 32]);
629        let expected = "ab".repeat(32);
630        assert_eq!(format!("{}", hash), expected);
631    }
632
633    #[test]
634    fn test_content_hash_serde_json_roundtrip() {
635        let hash = ContentHash::new([0xde; 32]);
636        let json = serde_json::to_string(&hash).expect("serialize");
637        // Should be a hex string in JSON
638        assert!(json.starts_with('"'));
639        let restored: ContentHash = serde_json::from_str(&json).expect("deserialize");
640        assert_eq!(hash, restored);
641    }
642
643    #[test]
644    fn test_content_hash_deserialize_from_array() {
645        let json_array: Vec<serde_json::Value> = (0u8..32).map(|b| serde_json::json!(b)).collect();
646        let json = serde_json::Value::Array(json_array);
647        let hash: ContentHash = serde_json::from_value(json).expect("deserialize from array");
648        for (i, &b) in hash.as_bytes().iter().enumerate() {
649            assert_eq!(b, i as u8);
650        }
651    }
652
653    #[test]
654    fn test_compute_content_hash_deterministic() {
655        let h1 = compute_content_hash(b"hello");
656        let h2 = compute_content_hash(b"hello");
657        assert_eq!(h1, h2);
658    }
659
660    #[test]
661    fn test_compute_content_hash_different_inputs() {
662        let h1 = compute_content_hash(b"hello");
663        let h2 = compute_content_hash(b"world");
664        assert_ne!(h1, h2);
665    }
666}