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}