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 with validation.
136            ///
137            /// Prefer this constructor at trust boundaries where the input may
138            /// come from users, HTTP payloads, or external systems.
139            pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
140                let value = value.into();
141                validate(&value, $kind)?;
142                Ok(Self(value))
143            }
144
145            /// Creates a typed identifier from a trusted string without
146            /// validation.
147            ///
148            /// Use this only for values that originate from internal runtime
149            /// paths (e.g. session service, runner) where the string is
150            /// already known to be valid. Prefer [`TryFrom`] or [`FromStr`]
151            /// at trust boundaries.
152            pub fn new_unchecked(value: impl Into<String>) -> Self {
153                Self(value.into())
154            }
155        }
156
157        impl AsRef<str> for $name {
158            fn as_ref(&self) -> &str {
159                &self.0
160            }
161        }
162
163        impl Borrow<str> for $name {
164            fn borrow(&self) -> &str {
165                &self.0
166            }
167        }
168
169        impl fmt::Display for $name {
170            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171                f.write_str(&self.0)
172            }
173        }
174
175        impl FromStr for $name {
176            type Err = IdentityError;
177
178            fn from_str(s: &str) -> Result<Self, Self::Err> {
179                validate(s, $kind)?;
180                Ok(Self(s.to_owned()))
181            }
182        }
183
184        impl TryFrom<&str> for $name {
185            type Error = IdentityError;
186
187            fn try_from(s: &str) -> Result<Self, Self::Error> {
188                validate(s, $kind)?;
189                Ok(Self(s.to_owned()))
190            }
191        }
192
193        impl TryFrom<String> for $name {
194            type Error = IdentityError;
195
196            fn try_from(s: String) -> Result<Self, Self::Error> {
197                validate(&s, $kind)?;
198                Ok(Self(s))
199            }
200        }
201    };
202}
203
204// ---------------------------------------------------------------------------
205// Leaf identifier types
206// ---------------------------------------------------------------------------
207
208define_id! {
209    /// A typed wrapper around the logical application name used in session
210    /// addressing.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use adk_core::identity::AppName;
216    ///
217    /// let app: AppName = "my-app".parse().unwrap();
218    /// assert_eq!(app.as_ref(), "my-app");
219    ///
220    /// // Empty values are rejected
221    /// assert!(AppName::try_from("").is_err());
222    /// ```
223    AppName, "AppName"
224}
225
226define_id! {
227    /// A typed wrapper around a logical user identifier.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use adk_core::identity::UserId;
233    ///
234    /// let uid: UserId = "tenant:alice@example.com".parse().unwrap();
235    /// assert_eq!(uid.as_ref(), "tenant:alice@example.com");
236    /// ```
237    UserId, "UserId"
238}
239
240define_id! {
241    /// A typed wrapper around a logical session identifier.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use adk_core::identity::SessionId;
247    ///
248    /// // Parse from a known value
249    /// let sid = SessionId::try_from("session-abc-123").unwrap();
250    /// assert_eq!(sid.as_ref(), "session-abc-123");
251    ///
252    /// // Or generate a new UUID-based session ID
253    /// let generated = SessionId::generate();
254    /// assert!(!generated.as_ref().is_empty());
255    /// ```
256    SessionId, "SessionId"
257}
258
259define_id! {
260    /// A typed wrapper around a single execution or turn identifier.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use adk_core::identity::InvocationId;
266    ///
267    /// // Parse from a known value
268    /// let iid = InvocationId::try_from("inv-001").unwrap();
269    /// assert_eq!(iid.as_ref(), "inv-001");
270    ///
271    /// // Or generate a new UUID-based invocation ID
272    /// let generated = InvocationId::generate();
273    /// assert!(!generated.as_ref().is_empty());
274    /// ```
275    InvocationId, "InvocationId"
276}
277
278// ---------------------------------------------------------------------------
279// Generation helpers
280// ---------------------------------------------------------------------------
281
282impl SessionId {
283    /// Generates a new random session identifier using UUID v4.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use adk_core::identity::SessionId;
289    ///
290    /// let a = SessionId::generate();
291    /// let b = SessionId::generate();
292    /// assert_ne!(a, b);
293    /// ```
294    pub fn generate() -> Self {
295        Self(Uuid::new_v4().to_string())
296    }
297}
298
299impl InvocationId {
300    /// Generates a new random invocation identifier using UUID v4.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use adk_core::identity::InvocationId;
306    ///
307    /// let a = InvocationId::generate();
308    /// let b = InvocationId::generate();
309    /// assert_ne!(a, b);
310    /// ```
311    pub fn generate() -> Self {
312        Self(Uuid::new_v4().to_string())
313    }
314}
315
316// ---------------------------------------------------------------------------
317// AdkIdentity
318// ---------------------------------------------------------------------------
319
320/// The stable session-scoped identity triple: application name, user, and
321/// session.
322///
323/// This is the natural addressing key for session-scoped operations. Passing
324/// an `AdkIdentity` instead of three separate strings eliminates parameter
325/// ordering bugs and makes the addressing model explicit.
326///
327/// # Display
328///
329/// The [`Display`](fmt::Display) implementation is diagnostic only and must
330/// not be parsed or used as a storage key.
331///
332/// # Examples
333///
334/// ```
335/// use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
336///
337/// let identity = AdkIdentity::new(
338///     AppName::try_from("weather-app").unwrap(),
339///     UserId::try_from("alice").unwrap(),
340///     SessionId::try_from("sess-1").unwrap(),
341/// );
342///
343/// assert_eq!(identity.app_name.as_ref(), "weather-app");
344/// assert_eq!(identity.user_id.as_ref(), "alice");
345/// assert_eq!(identity.session_id.as_ref(), "sess-1");
346/// ```
347#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
348pub struct AdkIdentity {
349    /// The application name.
350    pub app_name: AppName,
351    /// The user identifier.
352    pub user_id: UserId,
353    /// The session identifier.
354    pub session_id: SessionId,
355}
356
357impl AdkIdentity {
358    /// Creates a new `AdkIdentity` from its constituent parts.
359    pub fn new(app_name: AppName, user_id: UserId, session_id: SessionId) -> Self {
360        Self { app_name, user_id, session_id }
361    }
362}
363
364impl fmt::Display for AdkIdentity {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        write!(
367            f,
368            "AdkIdentity(app=\"{}\", user=\"{}\", session=\"{}\")",
369            self.app_name, self.user_id, self.session_id
370        )
371    }
372}
373
374// ---------------------------------------------------------------------------
375// ExecutionIdentity
376// ---------------------------------------------------------------------------
377
378/// The per-invocation execution identity, built from a stable
379/// [`AdkIdentity`] plus invocation-scoped metadata.
380///
381/// This is the runtime's internal identity capsule. It carries everything
382/// needed for event creation, telemetry correlation, and agent transfers
383/// without re-parsing raw strings.
384///
385/// # Examples
386///
387/// ```
388/// use adk_core::identity::{
389///     AdkIdentity, AppName, ExecutionIdentity, InvocationId, SessionId, UserId,
390/// };
391///
392/// let exec = ExecutionIdentity {
393///     adk: AdkIdentity::new(
394///         AppName::try_from("my-app").unwrap(),
395///         UserId::try_from("user-1").unwrap(),
396///         SessionId::try_from("sess-1").unwrap(),
397///     ),
398///     invocation_id: InvocationId::generate(),
399///     branch: String::new(),
400///     agent_name: "root".to_string(),
401/// };
402///
403/// assert_eq!(exec.adk.app_name.as_ref(), "my-app");
404/// ```
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct ExecutionIdentity {
407    /// The stable session-scoped identity.
408    pub adk: AdkIdentity,
409    /// The invocation identifier for this execution turn.
410    pub invocation_id: InvocationId,
411    /// The branch name. Defaults to an empty string in phase 1.
412    pub branch: String,
413    /// The name of the currently executing agent.
414    pub agent_name: String,
415}
416
417// ---------------------------------------------------------------------------
418// IdentityError -> AdkError conversion
419// ---------------------------------------------------------------------------
420
421impl From<IdentityError> for crate::AdkError {
422    fn from(err: IdentityError) -> Self {
423        crate::AdkError::config(err.to_string())
424    }
425}
426
427// ---------------------------------------------------------------------------
428// Tests
429// ---------------------------------------------------------------------------
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_app_name_valid() {
437        let app = AppName::try_from("my-app").unwrap();
438        assert_eq!(app.as_ref(), "my-app");
439        assert_eq!(app.to_string(), "my-app");
440    }
441
442    #[test]
443    fn test_app_name_with_special_chars() {
444        // Colons, slashes, and @ are allowed
445        let app = AppName::try_from("org:team/app@v2").unwrap();
446        assert_eq!(app.as_ref(), "org:team/app@v2");
447    }
448
449    #[test]
450    fn test_empty_rejected() {
451        let err = AppName::try_from("").unwrap_err();
452        assert_eq!(err, IdentityError::Empty { kind: "AppName" });
453    }
454
455    #[test]
456    fn test_null_byte_rejected() {
457        let err = UserId::try_from("user\0id").unwrap_err();
458        assert_eq!(err, IdentityError::ContainsNull { kind: "UserId" });
459    }
460
461    #[test]
462    fn test_too_long_rejected() {
463        let long = "x".repeat(MAX_ID_LEN + 1);
464        let err = SessionId::try_from(long.as_str()).unwrap_err();
465        assert_eq!(err, IdentityError::TooLong { kind: "SessionId", max: MAX_ID_LEN });
466    }
467
468    #[test]
469    fn test_max_length_accepted() {
470        let exact = "a".repeat(MAX_ID_LEN);
471        assert!(SessionId::try_from(exact.as_str()).is_ok());
472    }
473
474    #[test]
475    fn test_from_str() {
476        let app: AppName = "hello".parse().unwrap();
477        assert_eq!(app.as_ref(), "hello");
478    }
479
480    #[test]
481    fn test_try_from_string() {
482        let s = String::from("owned-value");
483        let uid = UserId::try_from(s).unwrap();
484        assert_eq!(uid.as_ref(), "owned-value");
485    }
486
487    #[test]
488    fn test_borrow_str() {
489        let sid = SessionId::try_from("sess-1").unwrap();
490        let borrowed: &str = sid.borrow();
491        assert_eq!(borrowed, "sess-1");
492    }
493
494    #[test]
495    fn test_ord() {
496        let a = AppName::try_from("aaa").unwrap();
497        let b = AppName::try_from("bbb").unwrap();
498        assert!(a < b);
499    }
500
501    #[test]
502    fn test_session_id_generate() {
503        let a = SessionId::generate();
504        let b = SessionId::generate();
505        assert_ne!(a, b);
506        assert!(!a.as_ref().is_empty());
507    }
508
509    #[test]
510    fn test_invocation_id_generate() {
511        let a = InvocationId::generate();
512        let b = InvocationId::generate();
513        assert_ne!(a, b);
514        assert!(!a.as_ref().is_empty());
515    }
516
517    #[test]
518    fn test_adk_identity_new() {
519        let identity = AdkIdentity::new(
520            AppName::try_from("app").unwrap(),
521            UserId::try_from("user").unwrap(),
522            SessionId::try_from("sess").unwrap(),
523        );
524        assert_eq!(identity.app_name.as_ref(), "app");
525        assert_eq!(identity.user_id.as_ref(), "user");
526        assert_eq!(identity.session_id.as_ref(), "sess");
527    }
528
529    #[test]
530    fn test_adk_identity_display() {
531        let identity = AdkIdentity::new(
532            AppName::try_from("weather-app").unwrap(),
533            UserId::try_from("alice").unwrap(),
534            SessionId::try_from("abc-123").unwrap(),
535        );
536        let display = identity.to_string();
537        assert!(display.contains("weather-app"));
538        assert!(display.contains("alice"));
539        assert!(display.contains("abc-123"));
540        assert!(display.starts_with("AdkIdentity("));
541    }
542
543    #[test]
544    fn test_adk_identity_equality() {
545        let a = AdkIdentity::new(
546            AppName::try_from("app").unwrap(),
547            UserId::try_from("user").unwrap(),
548            SessionId::try_from("sess").unwrap(),
549        );
550        let b = a.clone();
551        assert_eq!(a, b);
552
553        let c = AdkIdentity::new(
554            AppName::try_from("app").unwrap(),
555            UserId::try_from("other-user").unwrap(),
556            SessionId::try_from("sess").unwrap(),
557        );
558        assert_ne!(a, c);
559    }
560
561    #[test]
562    fn test_adk_identity_hash() {
563        use std::collections::HashSet;
564        let a = AdkIdentity::new(
565            AppName::try_from("app").unwrap(),
566            UserId::try_from("user").unwrap(),
567            SessionId::try_from("sess").unwrap(),
568        );
569        let b = a.clone();
570        let mut set = HashSet::new();
571        set.insert(a);
572        set.insert(b);
573        assert_eq!(set.len(), 1);
574    }
575
576    #[test]
577    fn test_execution_identity() {
578        let exec = ExecutionIdentity {
579            adk: AdkIdentity::new(
580                AppName::try_from("app").unwrap(),
581                UserId::try_from("user").unwrap(),
582                SessionId::try_from("sess").unwrap(),
583            ),
584            invocation_id: InvocationId::try_from("inv-1").unwrap(),
585            branch: String::new(),
586            agent_name: "root".to_string(),
587        };
588        assert_eq!(exec.adk.app_name.as_ref(), "app");
589        assert_eq!(exec.invocation_id.as_ref(), "inv-1");
590        assert_eq!(exec.branch, "");
591        assert_eq!(exec.agent_name, "root");
592    }
593
594    #[test]
595    fn test_serde_round_trip_leaf() {
596        let app = AppName::try_from("my-app").unwrap();
597        let json = serde_json::to_string(&app).unwrap();
598        assert_eq!(json, "\"my-app\"");
599        let deserialized: AppName = serde_json::from_str(&json).unwrap();
600        assert_eq!(app, deserialized);
601    }
602
603    #[test]
604    fn test_serde_round_trip_adk_identity() {
605        let identity = AdkIdentity::new(
606            AppName::try_from("app").unwrap(),
607            UserId::try_from("user").unwrap(),
608            SessionId::try_from("sess").unwrap(),
609        );
610        let json = serde_json::to_string(&identity).unwrap();
611        let deserialized: AdkIdentity = serde_json::from_str(&json).unwrap();
612        assert_eq!(identity, deserialized);
613    }
614
615    #[test]
616    fn test_serde_round_trip_execution_identity() {
617        let exec = ExecutionIdentity {
618            adk: AdkIdentity::new(
619                AppName::try_from("app").unwrap(),
620                UserId::try_from("user").unwrap(),
621                SessionId::try_from("sess").unwrap(),
622            ),
623            invocation_id: InvocationId::try_from("inv-1").unwrap(),
624            branch: "main".to_string(),
625            agent_name: "agent".to_string(),
626        };
627        let json = serde_json::to_string(&exec).unwrap();
628        let deserialized: ExecutionIdentity = serde_json::from_str(&json).unwrap();
629        assert_eq!(exec, deserialized);
630    }
631
632    #[test]
633    fn test_identity_error_display() {
634        let err = IdentityError::Empty { kind: "AppName" };
635        assert_eq!(err.to_string(), "AppName must not be empty");
636
637        let err = IdentityError::TooLong { kind: "UserId", max: 512 };
638        assert_eq!(err.to_string(), "UserId exceeds maximum length of 512 bytes");
639
640        let err = IdentityError::ContainsNull { kind: "SessionId" };
641        assert_eq!(err.to_string(), "SessionId must not contain null bytes");
642    }
643
644    #[test]
645    fn test_identity_error_to_adk_error() {
646        let err = IdentityError::Empty { kind: "AppName" };
647        let adk_err: crate::AdkError = err.into();
648        assert!(adk_err.is_config());
649        assert!(adk_err.to_string().contains("AppName must not be empty"));
650    }
651}