Skip to main content

agent_runtime_types/
lib.rs

1//! Stable identifiers shared across Runtime Domain boundaries.
2//!
3//! Identifiers validate at construction so empty or unbounded strings cannot
4//! enter domain state. Transport and provider types do not belong here.
5
6use std::{fmt, str::FromStr, sync::Arc};
7
8use serde::{Deserialize, Serialize};
9
10macro_rules! identifier {
11    ($name:ident, $label:literal) => {
12        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13        #[serde(transparent)]
14        pub struct $name(String);
15
16        impl $name {
17            pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
18                let value = value.into();
19                validate($label, &value)?;
20                Ok(Self(value))
21            }
22
23            pub fn as_str(&self) -> &str {
24                &self.0
25            }
26        }
27
28        impl fmt::Display for $name {
29            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30                self.0.fmt(formatter)
31            }
32        }
33
34        impl FromStr for $name {
35            type Err = IdentifierError;
36
37            fn from_str(value: &str) -> Result<Self, Self::Err> {
38                Self::new(value)
39            }
40        }
41    };
42}
43
44identifier!(ExecutionId, "execution_id");
45identifier!(ConversationId, "conversation_id");
46identifier!(RuntimeInstanceId, "runtime_instance_id");
47identifier!(WorkspaceId, "workspace_id");
48identifier!(ToolCallId, "tool_call_id");
49identifier!(OperationId, "operation_id");
50identifier!(EventId, "event_id");
51identifier!(DefinitionId, "definition_id");
52
53/// Opaque execution delegation reference. It is durable but cannot directly
54/// authenticate a request; Debug remains redacted because it is recovery data.
55#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
56#[serde(transparent)]
57pub struct DelegationLeaseRef(String);
58
59impl DelegationLeaseRef {
60    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
61        let value = value.into();
62        if !value.starts_with("edl_") {
63            return Err(IdentifierError {
64                label: "delegation_lease_ref",
65                reason: "must be an opaque edl_ reference",
66            });
67        }
68        validate("delegation_lease_ref", &value)?;
69        Ok(Self(value))
70    }
71
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75}
76
77impl fmt::Debug for DelegationLeaseRef {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str("DelegationLeaseRef([REDACTED OPAQUE REFERENCE])")
80    }
81}
82
83impl fmt::Display for DelegationLeaseRef {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str("[REDACTED OPAQUE REFERENCE]")
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase", deny_unknown_fields)]
91pub struct CallerScope {
92    pub subject: String,
93    pub tenant_id: String,
94    pub project_id: String,
95    #[serde(default)]
96    pub capabilities: Vec<String>,
97}
98
99impl CallerScope {
100    pub fn validate(&self) -> Result<(), IdentifierError> {
101        validate("subject", &self.subject)?;
102        validate("tenant_id", &self.tenant_id)?;
103        validate("project_id", &self.project_id)?;
104        Ok(())
105    }
106}
107
108/// Transient caller credential. It is intentionally not serializable and its
109/// Debug representation is always redacted.
110#[derive(Clone)]
111pub struct CredentialHandle(Arc<str>);
112
113impl CredentialHandle {
114    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentifierError> {
115        let value = value.into();
116        if value.is_empty() || value.len() > 16 * 1024 || value.chars().any(char::is_control) {
117            return Err(IdentifierError {
118                label: "credential",
119                reason: "must contain 1..=16384 non-control bytes",
120            });
121        }
122        Ok(Self(value))
123    }
124
125    pub fn expose(&self) -> &str {
126        &self.0
127    }
128}
129
130impl fmt::Debug for CredentialHandle {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter.write_str("CredentialHandle([REDACTED])")
133    }
134}
135
136#[derive(Clone, Debug)]
137pub struct RequestAuthority {
138    pub caller: CallerScope,
139    pub credential: CredentialHandle,
140}
141
142impl ExecutionId {
143    pub fn random() -> Self {
144        Self(uuid::Uuid::new_v4().to_string())
145    }
146}
147
148impl OperationId {
149    pub fn random() -> Self {
150        Self(uuid::Uuid::new_v4().to_string())
151    }
152}
153
154impl EventId {
155    pub fn random() -> Self {
156        Self(uuid::Uuid::new_v4().to_string())
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct IdentifierError {
162    label: &'static str,
163    reason: &'static str,
164}
165
166impl fmt::Display for IdentifierError {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(formatter, "{} {}", self.label, self.reason)
169    }
170}
171
172impl std::error::Error for IdentifierError {}
173
174fn validate(label: &'static str, value: &str) -> Result<(), IdentifierError> {
175    if value.is_empty() {
176        return Err(IdentifierError {
177            label,
178            reason: "must not be empty",
179        });
180    }
181    if value.len() > 256 {
182        return Err(IdentifierError {
183            label,
184            reason: "must not exceed 256 bytes",
185        });
186    }
187    if value.chars().any(char::is_control) {
188        return Err(IdentifierError {
189            label,
190            reason: "must not contain control characters",
191        });
192    }
193    Ok(())
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn identifiers_reject_empty_and_control_characters() {
202        assert!(ExecutionId::new("").is_err());
203        assert!(ExecutionId::new("bad\nvalue").is_err());
204        assert_eq!(
205            ExecutionId::new("execution-1").unwrap().as_str(),
206            "execution-1"
207        );
208    }
209}