Skip to main content

adk_core/
identity.rs

1//! Typed identity primitives for ADK-Rust.
2//!
3//! This module provides strongly-typed wrappers for the identity values used
4//! throughout the ADK runtime: application names, user identifiers, session
5//! identifiers, and invocation identifiers.
6//!
7//! ## Identity Layers
8//!
9//! ADK distinguishes three identity concerns:
10//!
11//! - **Auth identity** ([`RequestContext`](crate::RequestContext)): who is
12//!   authenticated and what scopes they hold.
13//! - **Session identity** ([`AdkIdentity`]): the stable `(app, user, session)`
14//!   triple that addresses a conversation session.
15//! - **Execution identity** ([`ExecutionIdentity`]): session identity plus the
16//!   per-invocation `invocation_id`, `branch`, and `agent_name`.
17//!
18//! ## Validation Rules
19//!
20//! All leaf identifiers ([`AppName`], [`UserId`], [`SessionId`],
21//! [`InvocationId`]) enforce the same validation:
22//!
23//! - Must not be empty.
24//! - Must not contain null bytes (`\0`).
25//! - Must not exceed [`MAX_ID_LEN`] bytes (512).
26//! - Characters like `:`, `|`, `/`, and `@` are allowed — validation does not
27//!   couple to any backend's internal key encoding.
28//!
29//! ## Examples
30//!
31//! ```
32//! use adk_core::identity::{AdkIdentity, AppName, ExecutionIdentity, InvocationId, SessionId, UserId};
33//!
34//! let identity = AdkIdentity::new(
35//!     AppName::try_from("weather-app").unwrap(),
36//!     UserId::try_from("tenant:alice@example.com").unwrap(),
37//!     SessionId::generate(),
38//! );
39//!
40//! let exec = ExecutionIdentity {
41//!     adk: identity,
42//!     invocation_id: InvocationId::generate(),
43//!     branch: String::new(),
44//!     agent_name: "planner".to_string(),
45//! };
46//! ```
47
48use serde::{Deserialize, Serialize};
49use std::borrow::Borrow;
50use std::fmt;
51use std::str::FromStr;
52use thiserror::Error;
53use uuid::Uuid;
54
55/// Maximum allowed length for any identity value, in bytes.
56pub const MAX_ID_LEN: usize = 512;
57
58// ---------------------------------------------------------------------------
59// IdentityError
60// ---------------------------------------------------------------------------
61
62/// Error returned when a raw string cannot be converted into a typed identifier.
63///
64/// # Examples
65///
66/// ```
67/// use adk_core::identity::{AppName, IdentityError};
68///
69/// let err = AppName::try_from("").unwrap_err();
70/// assert!(matches!(err, IdentityError::Empty { .. }));
71/// ```
72#[derive(Debug, Error, Clone, PartialEq, Eq)]
73pub enum IdentityError {
74    /// The input string was empty.
75    #[error("{kind} must not be empty")]
76    Empty {
77        /// Human-readable name of the identifier kind (e.g. `"AppName"`).
78        kind: &'static str,
79    },
80
81    /// The input string exceeded [`MAX_ID_LEN`].
82    #[error("{kind} exceeds maximum length of {max} bytes")]
83    TooLong {
84        /// Human-readable name of the identifier kind.
85        kind: &'static str,
86        /// The maximum allowed length.
87        max: usize,
88    },
89
90    /// The input string contained a null byte.
91    #[error("{kind} must not contain null bytes")]
92    ContainsNull {
93        /// Human-readable name of the identifier kind.
94        kind: &'static str,
95    },
96}
97
98// ---------------------------------------------------------------------------
99// Shared validation
100// ---------------------------------------------------------------------------
101
102fn validate(value: &str, kind: &'static str) -> Result<(), IdentityError> {
103    if value.is_empty() {
104        return Err(IdentityError::Empty { kind });
105    }
106    if value.len() > MAX_ID_LEN {
107        return Err(IdentityError::TooLong { kind, max: MAX_ID_LEN });
108    }
109    if value.contains('\0') {
110        return Err(IdentityError::ContainsNull { kind });
111    }
112    Ok(())
113}
114
115// ---------------------------------------------------------------------------
116// Macro for leaf identifier newtypes
117// ---------------------------------------------------------------------------
118
119macro_rules! define_id {
120    (
121        $(#[$meta:meta])*
122        $name:ident, $kind:literal
123    ) => {
124        $(#[$meta])*
125        #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
126        #[serde(transparent)]
127        pub struct $name(String);
128
129        impl $name {
130            /// Returns the inner string slice.
131            pub fn as_str(&self) -> &str {
132                &self.0
133            }
134
135            /// Creates a typed identifier from a trusted string without
136            /// validation.
137            ///
138            /// Use this only for values that originate from internal runtime
139            /// paths (e.g. session service, runner) where the string is
140            /// already known to be valid. Prefer [`TryFrom`] or [`FromStr`]
141            /// at trust boundaries.
142            pub fn new_unchecked(value: impl Into<String>) -> Self {
143                Self(value.into())
144            }
145        }
146
147        impl AsRef<str> for $name {
148            fn as_ref(&self) -> &str {
149                &self.0
150            }
151        }
152
153        impl Borrow<str> for $name {
154            fn borrow(&self) -> &str {
155                &self.0
156            }
157        }
158
159        impl fmt::Display for $name {
160            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161                f.write_str(&self.0)
162            }
163        }
164
165        impl FromStr for $name {
166            type Err = IdentityError;
167
168            fn from_str(s: &str) -> Result<Self, Self::Err> {
169                validate(s, $kind)?;
170                Ok(Self(s.to_owned()))
171            }
172        }
173
174        impl TryFrom<&str> for $name {
175            type Error = IdentityError;
176
177            fn try_from(s: &str) -> Result<Self, Self::Error> {
178                validate(s, $kind)?;
179                Ok(Self(s.to_owned()))
180            }
181        }
182
183        impl TryFrom<String> for $name {
184            type Error = IdentityError;
185
186            fn try_from(s: String) -> Result<Self, Self::Error> {
187                validate(&s, $kind)?;
188                Ok(Self(s))
189            }
190        }
191    };
192}
193
194// ---------------------------------------------------------------------------
195// Leaf identifier types
196// ---------------------------------------------------------------------------
197
198define_id! {
199    /// A typed wrapper around the logical application name used in session
200    /// addressing.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use adk_core::identity::AppName;
206    ///
207    /// let app: AppName = "my-app".parse().unwrap();
208    /// assert_eq!(app.as_ref(), "my-app");
209    ///
210    /// // Empty values are rejected
211    /// assert!(AppName::try_from("").is_err());
212    /// ```
213    AppName, "AppName"
214}
215
216define_id! {
217    /// A typed wrapper around a logical user identifier.
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// use adk_core::identity::UserId;
223    ///
224    /// let uid: UserId = "tenant:alice@example.com".parse().unwrap();
225    /// assert_eq!(uid.as_ref(), "tenant:alice@example.com");
226    /// ```
227    UserId, "UserId"
228}
229
230define_id! {
231    /// A typed wrapper around a logical session identifier.
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use adk_core::identity::SessionId;
237    ///
238    /// // Parse from a known value
239    /// let sid = SessionId::try_from("session-abc-123").unwrap();
240    /// assert_eq!(sid.as_ref(), "session-abc-123");
241    ///
242    /// // Or generate a new UUID-based session ID
243    /// let generated = SessionId::generate();
244    /// assert!(!generated.as_ref().is_empty());
245    /// ```
246    SessionId, "SessionId"
247}
248
249define_id! {
250    /// A typed wrapper around a single execution or turn identifier.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use adk_core::identity::InvocationId;
256    ///
257    /// // Parse from a known value
258    /// let iid = InvocationId::try_from("inv-001").unwrap();
259    /// assert_eq!(iid.as_ref(), "inv-001");
260    ///
261    /// // Or generate a new UUID-based invocation ID
262    /// let generated = InvocationId::generate();
263    /// assert!(!generated.as_ref().is_empty());
264    /// ```
265    InvocationId, "InvocationId"
266}
267
268// ---------------------------------------------------------------------------
269// Generation helpers
270// ---------------------------------------------------------------------------
271
272impl SessionId {
273    /// Generates a new random session identifier using UUID v4.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use adk_core::identity::SessionId;
279    ///
280    /// let a = SessionId::generate();
281    /// let b = SessionId::generate();
282    /// assert_ne!(a, b);
283    /// ```
284    pub fn generate() -> Self {
285        Self(Uuid::new_v4().to_string())
286    }
287}
288
289impl InvocationId {
290    /// Generates a new random invocation identifier using UUID v4.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use adk_core::identity::InvocationId;
296    ///
297    /// let a = InvocationId::generate();
298    /// let b = InvocationId::generate();
299    /// assert_ne!(a, b);
300    /// ```
301    pub fn generate() -> Self {
302        Self(Uuid::new_v4().to_string())
303    }
304}
305
306// ---------------------------------------------------------------------------
307// AdkIdentity
308// ---------------------------------------------------------------------------
309
310/// The stable session-scoped identity triple: application name, user, and
311/// session.
312///
313/// This is the natural addressing key for session-scoped operations. Passing
314/// an `AdkIdentity` instead of three separate strings eliminates parameter
315/// ordering bugs and makes the addressing model explicit.
316///
317/// # Display
318///
319/// The [`Display`](fmt::Display) implementation is diagnostic only and must
320/// not be parsed or used as a storage key.
321///
322/// # Examples
323///
324/// ```
325/// use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
326///
327/// let identity = AdkIdentity::new(
328///     AppName::try_from("weather-app").unwrap(),
329///     UserId::try_from("alice").unwrap(),
330///     SessionId::try_from("sess-1").unwrap(),
331/// );
332///
333/// assert_eq!(identity.app_name.as_ref(), "weather-app");
334/// assert_eq!(identity.user_id.as_ref(), "alice");
335/// assert_eq!(identity.session_id.as_ref(), "sess-1");
336/// ```
337#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
338pub struct AdkIdentity {
339    /// The application name.
340    pub app_name: AppName,
341    /// The user identifier.
342    pub user_id: UserId,
343    /// The session identifier.
344    pub session_id: SessionId,
345}
346
347impl AdkIdentity {
348    /// Creates a new `AdkIdentity` from its constituent parts.
349    pub fn new(app_name: AppName, user_id: UserId, session_id: SessionId) -> Self {
350        Self { app_name, user_id, session_id }
351    }
352}
353
354impl fmt::Display for AdkIdentity {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        write!(
357            f,
358            "AdkIdentity(app=\"{}\", user=\"{}\", session=\"{}\")",
359            self.app_name, self.user_id, self.session_id
360        )
361    }
362}
363
364// ---------------------------------------------------------------------------
365// ExecutionIdentity
366// ---------------------------------------------------------------------------
367
368/// The per-invocation execution identity, built from a stable
369/// [`AdkIdentity`] plus invocation-scoped metadata.
370///
371/// This is the runtime's internal identity capsule. It carries everything
372/// needed for event creation, telemetry correlation, and agent transfers
373/// without re-parsing raw strings.
374///
375/// # Examples
376///
377/// ```
378/// use adk_core::identity::{
379///     AdkIdentity, AppName, ExecutionIdentity, InvocationId, SessionId, UserId,
380/// };
381///
382/// let exec = ExecutionIdentity {
383///     adk: AdkIdentity::new(
384///         AppName::try_from("my-app").unwrap(),
385///         UserId::try_from("user-1").unwrap(),
386///         SessionId::try_from("sess-1").unwrap(),
387///     ),
388///     invocation_id: InvocationId::generate(),
389///     branch: String::new(),
390///     agent_name: "root".to_string(),
391/// };
392///
393/// assert_eq!(exec.adk.app_name.as_ref(), "my-app");
394/// ```
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct ExecutionIdentity {
397    /// The stable session-scoped identity.
398    pub adk: AdkIdentity,
399    /// The invocation identifier for this execution turn.
400    pub invocation_id: InvocationId,
401    /// The branch name. Defaults to an empty string in phase 1.
402    pub branch: String,
403    /// The name of the currently executing agent.
404    pub agent_name: String,
405}
406
407// ---------------------------------------------------------------------------
408// IdentityError -> AdkError conversion
409// ---------------------------------------------------------------------------
410
411impl From<IdentityError> for crate::AdkError {
412    fn from(err: IdentityError) -> Self {
413        crate::AdkError::Config(err.to_string())
414    }
415}
416
417// ---------------------------------------------------------------------------
418// Tests
419// ---------------------------------------------------------------------------
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_app_name_valid() {
427        let app = AppName::try_from("my-app").unwrap();
428        assert_eq!(app.as_ref(), "my-app");
429        assert_eq!(app.to_string(), "my-app");
430    }
431
432    #[test]
433    fn test_app_name_with_special_chars() {
434        // Colons, slashes, and @ are allowed
435        let app = AppName::try_from("org:team/app@v2").unwrap();
436        assert_eq!(app.as_ref(), "org:team/app@v2");
437    }
438
439    #[test]
440    fn test_empty_rejected() {
441        let err = AppName::try_from("").unwrap_err();
442        assert_eq!(err, IdentityError::Empty { kind: "AppName" });
443    }
444
445    #[test]
446    fn test_null_byte_rejected() {
447        let err = UserId::try_from("user\0id").unwrap_err();
448        assert_eq!(err, IdentityError::ContainsNull { kind: "UserId" });
449    }
450
451    #[test]
452    fn test_too_long_rejected() {
453        let long = "x".repeat(MAX_ID_LEN + 1);
454        let err = SessionId::try_from(long.as_str()).unwrap_err();
455        assert_eq!(err, IdentityError::TooLong { kind: "SessionId", max: MAX_ID_LEN });
456    }
457
458    #[test]
459    fn test_max_length_accepted() {
460        let exact = "a".repeat(MAX_ID_LEN);
461        assert!(SessionId::try_from(exact.as_str()).is_ok());
462    }
463
464    #[test]
465    fn test_from_str() {
466        let app: AppName = "hello".parse().unwrap();
467        assert_eq!(app.as_ref(), "hello");
468    }
469
470    #[test]
471    fn test_try_from_string() {
472        let s = String::from("owned-value");
473        let uid = UserId::try_from(s).unwrap();
474        assert_eq!(uid.as_ref(), "owned-value");
475    }
476
477    #[test]
478    fn test_borrow_str() {
479        let sid = SessionId::try_from("sess-1").unwrap();
480        let borrowed: &str = sid.borrow();
481        assert_eq!(borrowed, "sess-1");
482    }
483
484    #[test]
485    fn test_ord() {
486        let a = AppName::try_from("aaa").unwrap();
487        let b = AppName::try_from("bbb").unwrap();
488        assert!(a < b);
489    }
490
491    #[test]
492    fn test_session_id_generate() {
493        let a = SessionId::generate();
494        let b = SessionId::generate();
495        assert_ne!(a, b);
496        assert!(!a.as_ref().is_empty());
497    }
498
499    #[test]
500    fn test_invocation_id_generate() {
501        let a = InvocationId::generate();
502        let b = InvocationId::generate();
503        assert_ne!(a, b);
504        assert!(!a.as_ref().is_empty());
505    }
506
507    #[test]
508    fn test_adk_identity_new() {
509        let identity = AdkIdentity::new(
510            AppName::try_from("app").unwrap(),
511            UserId::try_from("user").unwrap(),
512            SessionId::try_from("sess").unwrap(),
513        );
514        assert_eq!(identity.app_name.as_ref(), "app");
515        assert_eq!(identity.user_id.as_ref(), "user");
516        assert_eq!(identity.session_id.as_ref(), "sess");
517    }
518
519    #[test]
520    fn test_adk_identity_display() {
521        let identity = AdkIdentity::new(
522            AppName::try_from("weather-app").unwrap(),
523            UserId::try_from("alice").unwrap(),
524            SessionId::try_from("abc-123").unwrap(),
525        );
526        let display = identity.to_string();
527        assert!(display.contains("weather-app"));
528        assert!(display.contains("alice"));
529        assert!(display.contains("abc-123"));
530        assert!(display.starts_with("AdkIdentity("));
531    }
532
533    #[test]
534    fn test_adk_identity_equality() {
535        let a = AdkIdentity::new(
536            AppName::try_from("app").unwrap(),
537            UserId::try_from("user").unwrap(),
538            SessionId::try_from("sess").unwrap(),
539        );
540        let b = a.clone();
541        assert_eq!(a, b);
542
543        let c = AdkIdentity::new(
544            AppName::try_from("app").unwrap(),
545            UserId::try_from("other-user").unwrap(),
546            SessionId::try_from("sess").unwrap(),
547        );
548        assert_ne!(a, c);
549    }
550
551    #[test]
552    fn test_adk_identity_hash() {
553        use std::collections::HashSet;
554        let a = AdkIdentity::new(
555            AppName::try_from("app").unwrap(),
556            UserId::try_from("user").unwrap(),
557            SessionId::try_from("sess").unwrap(),
558        );
559        let b = a.clone();
560        let mut set = HashSet::new();
561        set.insert(a);
562        set.insert(b);
563        assert_eq!(set.len(), 1);
564    }
565
566    #[test]
567    fn test_execution_identity() {
568        let exec = ExecutionIdentity {
569            adk: AdkIdentity::new(
570                AppName::try_from("app").unwrap(),
571                UserId::try_from("user").unwrap(),
572                SessionId::try_from("sess").unwrap(),
573            ),
574            invocation_id: InvocationId::try_from("inv-1").unwrap(),
575            branch: String::new(),
576            agent_name: "root".to_string(),
577        };
578        assert_eq!(exec.adk.app_name.as_ref(), "app");
579        assert_eq!(exec.invocation_id.as_ref(), "inv-1");
580        assert_eq!(exec.branch, "");
581        assert_eq!(exec.agent_name, "root");
582    }
583
584    #[test]
585    fn test_serde_round_trip_leaf() {
586        let app = AppName::try_from("my-app").unwrap();
587        let json = serde_json::to_string(&app).unwrap();
588        assert_eq!(json, "\"my-app\"");
589        let deserialized: AppName = serde_json::from_str(&json).unwrap();
590        assert_eq!(app, deserialized);
591    }
592
593    #[test]
594    fn test_serde_round_trip_adk_identity() {
595        let identity = AdkIdentity::new(
596            AppName::try_from("app").unwrap(),
597            UserId::try_from("user").unwrap(),
598            SessionId::try_from("sess").unwrap(),
599        );
600        let json = serde_json::to_string(&identity).unwrap();
601        let deserialized: AdkIdentity = serde_json::from_str(&json).unwrap();
602        assert_eq!(identity, deserialized);
603    }
604
605    #[test]
606    fn test_serde_round_trip_execution_identity() {
607        let exec = ExecutionIdentity {
608            adk: AdkIdentity::new(
609                AppName::try_from("app").unwrap(),
610                UserId::try_from("user").unwrap(),
611                SessionId::try_from("sess").unwrap(),
612            ),
613            invocation_id: InvocationId::try_from("inv-1").unwrap(),
614            branch: "main".to_string(),
615            agent_name: "agent".to_string(),
616        };
617        let json = serde_json::to_string(&exec).unwrap();
618        let deserialized: ExecutionIdentity = serde_json::from_str(&json).unwrap();
619        assert_eq!(exec, deserialized);
620    }
621
622    #[test]
623    fn test_identity_error_display() {
624        let err = IdentityError::Empty { kind: "AppName" };
625        assert_eq!(err.to_string(), "AppName must not be empty");
626
627        let err = IdentityError::TooLong { kind: "UserId", max: 512 };
628        assert_eq!(err.to_string(), "UserId exceeds maximum length of 512 bytes");
629
630        let err = IdentityError::ContainsNull { kind: "SessionId" };
631        assert_eq!(err.to_string(), "SessionId must not contain null bytes");
632    }
633
634    #[test]
635    fn test_identity_error_to_adk_error() {
636        let err = IdentityError::Empty { kind: "AppName" };
637        let adk_err: crate::AdkError = err.into();
638        assert!(matches!(adk_err, crate::AdkError::Config(_)));
639        assert!(adk_err.to_string().contains("AppName must not be empty"));
640    }
641}