tower_a2a/protocol/
agent.rs

1//! Agent discovery and capability types
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use url::Url;
8use uuid::Uuid;
9
10/// Agent scope for granular access control
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase")]
13pub struct AgentScope {
14    pub scope_type: String,
15    pub scope_id: Uuid,
16}
17
18/// Agent Card for agent discovery
19///
20/// The Agent Card is published at `/.well-known/agent-card.json` and describes
21/// the agent's capabilities, supported interfaces, and authentication requirements.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23pub struct AgentCard {
24    /// Database ID (not part of protocol, used for database storage)
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub id: Option<Uuid>,
27
28    /// Organization ID (not part of protocol, used for multi-tenancy)
29    #[serde(
30        rename = "organizationId",
31        default,
32        skip_serializing_if = "Option::is_none"
33    )]
34    pub organization_id: Option<Uuid>,
35
36    /// Name of the agent
37    pub name: String,
38
39    /// Human-readable description of the agent
40    pub description: String,
41
42    /// Agent capabilities
43    pub capabilities: AgentCapabilities,
44
45    /// Supported authentication schemes
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub authentication: Option<Vec<SecurityScheme>>,
48
49    /// Endpoint configurations for different bindings
50    pub endpoints: HashMap<String, EndpointConfig>,
51
52    /// Agent version
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub version: Option<String>,
55
56    /// URL to agent documentation
57    #[serde(rename = "documentationUrl", skip_serializing_if = "Option::is_none")]
58    pub documentation_url: Option<String>,
59
60    /// Granular access control scopes (not part of protocol, used for authorization)
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub scopes: Option<Vec<AgentScope>>,
63}
64
65impl AgentCard {
66    /// Create a new agent card
67    pub fn new(
68        name: impl Into<String>,
69        description: impl Into<String>,
70        capabilities: AgentCapabilities,
71    ) -> Self {
72        Self {
73            id: None,
74            organization_id: None,
75            name: name.into(),
76            description: description.into(),
77            capabilities,
78            authentication: None,
79            endpoints: HashMap::new(),
80            version: None,
81            documentation_url: None,
82            scopes: None,
83        }
84    }
85
86    /// Add an endpoint to the agent card
87    pub fn with_endpoint(mut self, name: impl Into<String>, config: EndpointConfig) -> Self {
88        self.endpoints.insert(name.into(), config);
89        self
90    }
91
92    /// Add authentication schemes
93    pub fn with_authentication(mut self, schemes: Vec<SecurityScheme>) -> Self {
94        self.authentication = Some(schemes);
95        self
96    }
97
98    /// Set the agent version
99    pub fn with_version(mut self, version: impl Into<String>) -> Self {
100        self.version = Some(version.into());
101        self
102    }
103}
104
105/// Agent capabilities
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub struct AgentCapabilities {
108    /// Supports streaming responses
109    #[serde(default)]
110    pub streaming: bool,
111
112    /// Supports push notifications via webhooks
113    #[serde(rename = "pushNotifications", default)]
114    pub push_notifications: bool,
115
116    /// Supports task management (get, list, cancel)
117    #[serde(rename = "taskManagement", default)]
118    pub task_management: bool,
119
120    /// Supports multi-turn conversations with context
121    #[serde(rename = "multiTurn", default)]
122    pub multi_turn: bool,
123
124    /// Supported message part types
125    #[serde(rename = "supportedPartTypes", skip_serializing_if = "Option::is_none")]
126    pub supported_part_types: Option<Vec<String>>,
127}
128
129impl AgentCapabilities {
130    /// Create capabilities with default values (all false)
131    pub fn new() -> Self {
132        Self {
133            streaming: false,
134            push_notifications: false,
135            task_management: false,
136            multi_turn: false,
137            supported_part_types: None,
138        }
139    }
140
141    /// Enable streaming
142    pub fn with_streaming(mut self) -> Self {
143        self.streaming = true;
144        self
145    }
146
147    /// Enable push notifications
148    pub fn with_push_notifications(mut self) -> Self {
149        self.push_notifications = true;
150        self
151    }
152
153    /// Enable task management
154    pub fn with_task_management(mut self) -> Self {
155        self.task_management = true;
156        self
157    }
158
159    /// Enable multi-turn conversations
160    pub fn with_multi_turn(mut self) -> Self {
161        self.multi_turn = true;
162        self
163    }
164}
165
166impl Default for AgentCapabilities {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// API Key security scheme
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174#[serde(rename_all = "camelCase")]
175pub struct ApiKeySecurityScheme {
176    pub description: Option<String>,
177    #[serde(rename = "in")]
178    pub location: String,
179    pub name: String,
180}
181
182/// HTTP authentication security scheme
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184#[serde(rename_all = "camelCase")]
185pub struct HttpAuthSecurityScheme {
186    pub description: Option<String>,
187    pub scheme: String,
188    pub bearer_format: Option<String>,
189}
190
191/// OAuth flow configuration
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
193#[serde(rename_all = "camelCase")]
194pub struct OAuthFlow {
195    pub authorization_url: Option<Url>,
196    pub token_url: Option<Url>,
197    pub refresh_url: Option<Url>,
198    pub scopes: HashMap<String, String>,
199}
200
201/// OAuth flows configuration
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
203#[serde(rename_all = "camelCase")]
204pub struct OAuthFlows {
205    pub authorization_code: Option<OAuthFlow>,
206    pub client_credentials: Option<OAuthFlow>,
207    pub implicit: Option<OAuthFlow>,
208    pub password: Option<OAuthFlow>,
209}
210
211/// OAuth2 security scheme
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[serde(rename_all = "camelCase")]
214pub struct OAuth2SecurityScheme {
215    pub description: Option<String>,
216    pub flows: OAuthFlows,
217    pub oauth2_metadata_url: Option<Url>,
218}
219
220/// OpenID Connect security scheme
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
222#[serde(rename_all = "camelCase")]
223pub struct OpenIdConnectSecurityScheme {
224    pub description: Option<String>,
225    pub open_id_connect_url: Url,
226}
227
228/// Security scheme for authentication
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub enum SecurityScheme {
231    #[serde(rename = "apiKeySecurityScheme")]
232    ApiKey(ApiKeySecurityScheme),
233    #[serde(rename = "httpAuthSecurityScheme")]
234    HttpAuth(HttpAuthSecurityScheme),
235    #[serde(rename = "oauth2SecurityScheme")]
236    OAuth2(Box<OAuth2SecurityScheme>),
237    #[serde(rename = "openIdConnectSecurityScheme")]
238    OpenIdConnect(OpenIdConnectSecurityScheme),
239}
240
241/// Endpoint configuration for a specific binding
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243pub struct EndpointConfig {
244    /// Base URL for the endpoint
245    pub url: String,
246
247    /// Protocol/binding type (e.g., "http+json", "grpc", "json-rpc")
248    #[serde(rename = "type")]
249    pub endpoint_type: String,
250
251    /// Whether this endpoint is preferred
252    #[serde(default)]
253    pub preferred: bool,
254}
255
256impl EndpointConfig {
257    /// Create a new endpoint configuration
258    pub fn new(url: impl Into<String>, endpoint_type: impl Into<String>) -> Self {
259        Self {
260            url: url.into(),
261            endpoint_type: endpoint_type.into(),
262            preferred: false,
263        }
264    }
265
266    /// Mark this endpoint as preferred
267    pub fn preferred(mut self) -> Self {
268        self.preferred = true;
269        self
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_agent_card_creation() {
279        let capabilities = AgentCapabilities::new()
280            .with_streaming()
281            .with_task_management();
282
283        let card = AgentCard::new("Test Agent", "A test agent", capabilities)
284            .with_version("1.0.0")
285            .with_endpoint(
286                "http",
287                EndpointConfig::new("https://example.com", "http+json").preferred(),
288            );
289
290        assert_eq!(card.name, "Test Agent");
291        assert!(card.capabilities.streaming);
292        assert!(card.capabilities.task_management);
293        assert_eq!(card.version, Some("1.0.0".to_string()));
294        assert_eq!(card.endpoints.len(), 1);
295    }
296
297    #[test]
298    fn test_agent_capabilities() {
299        let mut caps = AgentCapabilities::default();
300        assert!(!caps.streaming);
301        assert!(!caps.task_management);
302
303        caps = caps.with_streaming().with_multi_turn();
304        assert!(caps.streaming);
305        assert!(caps.multi_turn);
306    }
307
308    #[test]
309    fn test_security_schemes() {
310        let http_auth = SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
311            description: None,
312            scheme: "bearer".to_string(),
313            bearer_format: None,
314        });
315        let api_key = SecurityScheme::ApiKey(ApiKeySecurityScheme {
316            description: None,
317            location: "header".to_string(),
318            name: "X-API-Key".to_string(),
319        });
320
321        assert!(matches!(http_auth, SecurityScheme::HttpAuth(_)));
322        assert!(matches!(api_key, SecurityScheme::ApiKey(_)));
323    }
324
325    #[test]
326    fn test_agent_card_serialization() {
327        let capabilities = AgentCapabilities::new().with_streaming();
328        let card = AgentCard::new("Test", "Description", capabilities);
329
330        let json = serde_json::to_string(&card).unwrap();
331        assert!(json.contains("\"name\":\"Test\""));
332
333        let deserialized: AgentCard = serde_json::from_str(&json).unwrap();
334        assert_eq!(card, deserialized);
335    }
336}