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}