Skip to main content

rullama_a2a/
agent_card.rs

1//! Agent card types: AgentCard, AgentCapabilities, AgentSkill, security schemes, OAuth flows.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Self-describing manifest for an agent.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentCard {
9    /// Human-readable agent name.
10    pub name: String,
11    /// Human-readable description.
12    pub description: String,
13    /// Agent version string.
14    pub version: String,
15    /// Ordered list of supported interfaces (first is preferred).
16    #[serde(rename = "supportedInterfaces")]
17    pub supported_interfaces: Vec<AgentInterface>,
18    /// Agent capabilities.
19    pub capabilities: AgentCapabilities,
20    /// Agent skills.
21    pub skills: Vec<AgentSkill>,
22    /// Default input media types.
23    #[serde(rename = "defaultInputModes")]
24    pub default_input_modes: Vec<String>,
25    /// Default output media types.
26    #[serde(rename = "defaultOutputModes")]
27    pub default_output_modes: Vec<String>,
28    /// Service provider information.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub provider: Option<AgentProvider>,
31    /// Security scheme definitions.
32    #[serde(rename = "securitySchemes", skip_serializing_if = "Option::is_none")]
33    pub security_schemes: Option<HashMap<String, SecurityScheme>>,
34    /// Security requirements.
35    #[serde(
36        rename = "securityRequirements",
37        skip_serializing_if = "Option::is_none"
38    )]
39    pub security_requirements: Option<Vec<SecurityRequirement>>,
40    /// URL to additional documentation.
41    #[serde(rename = "documentationUrl", skip_serializing_if = "Option::is_none")]
42    pub documentation_url: Option<String>,
43    /// Icon URL.
44    #[serde(rename = "iconUrl", skip_serializing_if = "Option::is_none")]
45    pub icon_url: Option<String>,
46    /// JWS signatures for the agent card.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub signatures: Option<Vec<AgentCardSignature>>,
49}
50
51/// Declares a protocol binding interface for the agent.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AgentInterface {
54    /// URL where this interface is available.
55    pub url: String,
56    /// Protocol binding: `JSONRPC`, `GRPC`, `HTTP+JSON`.
57    #[serde(rename = "protocolBinding")]
58    pub protocol_binding: String,
59    /// Optional tenant identifier.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub tenant: Option<String>,
62    /// A2A protocol version. Use [`crate::A2A_PROTOCOL_VERSION`] for the canonical value.
63    #[serde(rename = "protocolVersion")]
64    pub protocol_version: String,
65}
66
67/// Agent capabilities.
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69pub struct AgentCapabilities {
70    /// Supports streaming responses.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub streaming: Option<bool>,
73    /// Supports push notifications.
74    #[serde(rename = "pushNotifications", skip_serializing_if = "Option::is_none")]
75    pub push_notifications: Option<bool>,
76    /// Supports extended agent card.
77    #[serde(rename = "extendedAgentCard", skip_serializing_if = "Option::is_none")]
78    pub extended_agent_card: Option<bool>,
79    /// Protocol extensions supported.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub extensions: Option<Vec<AgentExtension>>,
82}
83
84/// A protocol extension declaration.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AgentExtension {
87    /// Unique URI identifying the extension.
88    pub uri: String,
89    /// Human-readable description.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub description: Option<String>,
92    /// Whether the client must comply.
93    #[serde(default)]
94    pub required: bool,
95    /// Extension-specific parameters.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub params: Option<HashMap<String, serde_json::Value>>,
98}
99
100/// An agent's specific capability or function.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct AgentSkill {
103    /// Unique skill identifier.
104    pub id: String,
105    /// Human-readable skill name.
106    pub name: String,
107    /// Detailed description.
108    pub description: String,
109    /// Keywords describing capabilities.
110    pub tags: Vec<String>,
111    /// Example prompts/scenarios.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub examples: Option<Vec<String>>,
114    /// Override input modes for this skill.
115    #[serde(rename = "inputModes", skip_serializing_if = "Option::is_none")]
116    pub input_modes: Option<Vec<String>>,
117    /// Override output modes for this skill.
118    #[serde(rename = "outputModes", skip_serializing_if = "Option::is_none")]
119    pub output_modes: Option<Vec<String>>,
120    /// Security requirements for this skill.
121    #[serde(
122        rename = "securityRequirements",
123        skip_serializing_if = "Option::is_none"
124    )]
125    pub security_requirements: Option<Vec<SecurityRequirement>>,
126}
127
128/// Agent service provider.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct AgentProvider {
131    /// Provider website or documentation URL.
132    pub url: String,
133    /// Organization name.
134    pub organization: String,
135}
136
137/// JWS signature for an AgentCard (RFC 7515).
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AgentCardSignature {
140    /// Base64url-encoded protected JWS header.
141    pub protected: String,
142    /// Base64url-encoded computed signature.
143    pub signature: String,
144    /// Unprotected header values.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub header: Option<HashMap<String, serde_json::Value>>,
147}
148
149/// Security requirements map: scheme name -> required scopes.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct SecurityRequirement {
152    /// Map of security scheme names to their required scopes.
153    pub schemes: HashMap<String, Vec<String>>,
154}
155
156/// Security scheme (wrapper-based oneOf).
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct SecurityScheme {
159    /// API key authentication.
160    #[serde(
161        skip_serializing_if = "Option::is_none",
162        rename = "apiKeySecurityScheme"
163    )]
164    pub api_key: Option<ApiKeySecurityScheme>,
165    /// HTTP authentication (Bearer, Basic, etc).
166    #[serde(
167        skip_serializing_if = "Option::is_none",
168        rename = "httpAuthSecurityScheme"
169    )]
170    pub http_auth: Option<HttpAuthSecurityScheme>,
171    /// OAuth 2.0 authentication.
172    #[serde(
173        skip_serializing_if = "Option::is_none",
174        rename = "oauth2SecurityScheme"
175    )]
176    pub oauth2: Option<OAuth2SecurityScheme>,
177    /// OpenID Connect authentication.
178    #[serde(
179        skip_serializing_if = "Option::is_none",
180        rename = "openIdConnectSecurityScheme"
181    )]
182    pub open_id_connect: Option<OpenIdConnectSecurityScheme>,
183    /// Mutual TLS authentication.
184    #[serde(skip_serializing_if = "Option::is_none", rename = "mtlsSecurityScheme")]
185    pub mtls: Option<MutualTlsSecurityScheme>,
186}
187
188/// API key security scheme details.
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct ApiKeySecurityScheme {
191    /// Parameter name.
192    pub name: String,
193    /// Location: `query`, `header`, or `cookie`.
194    #[serde(rename = "in")]
195    pub location: String,
196    /// Description.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub description: Option<String>,
199}
200
201/// HTTP authentication security scheme details.
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
203pub struct HttpAuthSecurityScheme {
204    /// Auth scheme name (e.g. `Bearer`).
205    pub scheme: String,
206    /// Format hint (e.g. `JWT`).
207    #[serde(rename = "bearerFormat", skip_serializing_if = "Option::is_none")]
208    pub bearer_format: Option<String>,
209    /// Description.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub description: Option<String>,
212}
213
214/// OAuth 2.0 security scheme details.
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct OAuth2SecurityScheme {
217    /// OAuth2 flow configuration.
218    pub flows: OAuthFlows,
219    /// Description.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub description: Option<String>,
222    /// OAuth2 metadata URL (RFC 8414).
223    #[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
224    pub oauth2_metadata_url: Option<String>,
225}
226
227/// OpenID Connect security scheme details.
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229pub struct OpenIdConnectSecurityScheme {
230    /// OIDC discovery URL.
231    #[serde(rename = "openIdConnectUrl")]
232    pub open_id_connect_url: String,
233    /// Description.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub description: Option<String>,
236}
237
238/// Mutual TLS security scheme details.
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
240pub struct MutualTlsSecurityScheme {
241    /// Description.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub description: Option<String>,
244}
245
246/// OAuth 2.0 flow configuration (wrapper-based oneOf).
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct OAuthFlows {
249    /// Authorization Code flow.
250    #[serde(skip_serializing_if = "Option::is_none", rename = "authorizationCode")]
251    pub authorization_code: Option<AuthorizationCodeOAuthFlow>,
252    /// Client Credentials flow.
253    #[serde(skip_serializing_if = "Option::is_none", rename = "clientCredentials")]
254    pub client_credentials: Option<ClientCredentialsOAuthFlow>,
255    /// Implicit flow (deprecated).
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub implicit: Option<ImplicitOAuthFlow>,
258    /// Password flow (deprecated).
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub password: Option<PasswordOAuthFlow>,
261    /// Device Code flow (RFC 8628).
262    #[serde(skip_serializing_if = "Option::is_none", rename = "deviceCode")]
263    pub device_code: Option<DeviceCodeOAuthFlow>,
264}
265
266/// Authorization Code OAuth flow.
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct AuthorizationCodeOAuthFlow {
269    /// Authorization URL.
270    #[serde(rename = "authorizationUrl")]
271    pub authorization_url: String,
272    /// Token URL.
273    #[serde(rename = "tokenUrl")]
274    pub token_url: String,
275    /// Refresh URL.
276    #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
277    pub refresh_url: Option<String>,
278    /// Available scopes.
279    pub scopes: HashMap<String, String>,
280    /// Whether PKCE is required.
281    #[serde(rename = "pkceRequired", skip_serializing_if = "Option::is_none")]
282    pub pkce_required: Option<bool>,
283}
284
285/// Client Credentials OAuth flow.
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct ClientCredentialsOAuthFlow {
288    /// Token URL.
289    #[serde(rename = "tokenUrl")]
290    pub token_url: String,
291    /// Refresh URL.
292    #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
293    pub refresh_url: Option<String>,
294    /// Available scopes.
295    pub scopes: HashMap<String, String>,
296}
297
298/// Implicit OAuth flow (deprecated).
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct ImplicitOAuthFlow {
301    /// Authorization URL.
302    #[serde(rename = "authorizationUrl", skip_serializing_if = "Option::is_none")]
303    pub authorization_url: Option<String>,
304    /// Refresh URL.
305    #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
306    pub refresh_url: Option<String>,
307    /// Available scopes.
308    #[serde(default)]
309    pub scopes: HashMap<String, String>,
310}
311
312/// Password OAuth flow (deprecated).
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314pub struct PasswordOAuthFlow {
315    /// Token URL.
316    #[serde(rename = "tokenUrl", skip_serializing_if = "Option::is_none")]
317    pub token_url: Option<String>,
318    /// Refresh URL.
319    #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
320    pub refresh_url: Option<String>,
321    /// Available scopes.
322    #[serde(default)]
323    pub scopes: HashMap<String, String>,
324}
325
326/// Device Code OAuth flow (RFC 8628).
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328pub struct DeviceCodeOAuthFlow {
329    /// Device authorization URL.
330    #[serde(rename = "deviceAuthorizationUrl")]
331    pub device_authorization_url: String,
332    /// Token URL.
333    #[serde(rename = "tokenUrl")]
334    pub token_url: String,
335    /// Refresh URL.
336    #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
337    pub refresh_url: Option<String>,
338    /// Available scopes.
339    pub scopes: HashMap<String, String>,
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    fn minimal_card() -> AgentCard {
347        AgentCard {
348            name: "Test Agent".to_string(),
349            description: "A test agent".to_string(),
350            version: "1.0.0".to_string(),
351            supported_interfaces: vec![AgentInterface {
352                url: "https://example.com/a2a".to_string(),
353                protocol_binding: "JSONRPC".to_string(),
354                tenant: None,
355                protocol_version: "0.3".to_string(),
356            }],
357            capabilities: AgentCapabilities::default(),
358            skills: vec![],
359            default_input_modes: vec!["text/plain".to_string()],
360            default_output_modes: vec!["text/plain".to_string()],
361            provider: None,
362            security_schemes: None,
363            security_requirements: None,
364            documentation_url: None,
365            icon_url: None,
366            signatures: None,
367        }
368    }
369
370    // --- AgentCard ---
371
372    #[test]
373    fn agent_card_roundtrip() {
374        let card = minimal_card();
375        let json = serde_json::to_string(&card).unwrap();
376        let back: AgentCard = serde_json::from_str(&json).unwrap();
377        assert_eq!(back.name, card.name);
378        assert_eq!(back.version, card.version);
379    }
380
381    #[test]
382    fn agent_card_json_uses_camel_case() {
383        let card = minimal_card();
384        let json = serde_json::to_string(&card).unwrap();
385        assert!(json.contains("supportedInterfaces"));
386        assert!(json.contains("defaultInputModes"));
387        assert!(json.contains("defaultOutputModes"));
388    }
389
390    #[test]
391    fn optional_fields_omitted_when_none() {
392        let card = minimal_card();
393        let json = serde_json::to_string(&card).unwrap();
394        assert!(!json.contains("provider"));
395        assert!(!json.contains("securitySchemes"));
396        assert!(!json.contains("documentationUrl"));
397        assert!(!json.contains("iconUrl"));
398        assert!(!json.contains("signatures"));
399    }
400
401    #[test]
402    fn agent_card_with_skill_roundtrip() {
403        let mut card = minimal_card();
404        card.skills = vec![AgentSkill {
405            id: "skill-1".to_string(),
406            name: "My Skill".to_string(),
407            description: "Does something".to_string(),
408            tags: vec!["tag1".to_string()],
409            examples: Some(vec!["example prompt".to_string()]),
410            input_modes: None,
411            output_modes: None,
412            security_requirements: None,
413        }];
414        let json = serde_json::to_string(&card).unwrap();
415        let back: AgentCard = serde_json::from_str(&json).unwrap();
416        assert_eq!(back.skills.len(), 1);
417        assert_eq!(back.skills[0].id, "skill-1");
418    }
419
420    // --- AgentCapabilities ---
421
422    #[test]
423    fn capabilities_default_all_none() {
424        let cap = AgentCapabilities::default();
425        let json = serde_json::to_string(&cap).unwrap();
426        assert!(!json.contains("streaming"));
427        assert!(!json.contains("pushNotifications"));
428        assert!(!json.contains("extendedAgentCard"));
429    }
430
431    #[test]
432    fn capabilities_with_streaming_roundtrip() {
433        let cap = AgentCapabilities {
434            streaming: Some(true),
435            push_notifications: Some(false),
436            extended_agent_card: None,
437            extensions: None,
438        };
439        let json = serde_json::to_string(&cap).unwrap();
440        let back: AgentCapabilities = serde_json::from_str(&json).unwrap();
441        assert_eq!(back.streaming, Some(true));
442        assert_eq!(back.push_notifications, Some(false));
443    }
444
445    // --- SecurityScheme ---
446
447    #[test]
448    fn api_key_security_scheme_roundtrip() {
449        let scheme = SecurityScheme {
450            api_key: Some(ApiKeySecurityScheme {
451                name: "X-Api-Key".to_string(),
452                location: "header".to_string(),
453                description: None,
454            }),
455            http_auth: None,
456            oauth2: None,
457            open_id_connect: None,
458            mtls: None,
459        };
460        let json = serde_json::to_string(&scheme).unwrap();
461        let back: SecurityScheme = serde_json::from_str(&json).unwrap();
462        assert_eq!(back, scheme);
463        assert!(json.contains("apiKeySecurityScheme"));
464    }
465
466    #[test]
467    fn oauth2_authorization_code_flow_roundtrip() {
468        let scheme = SecurityScheme {
469            api_key: None,
470            http_auth: None,
471            oauth2: Some(OAuth2SecurityScheme {
472                flows: OAuthFlows {
473                    authorization_code: Some(AuthorizationCodeOAuthFlow {
474                        authorization_url: "https://auth.example.com/authorize".to_string(),
475                        token_url: "https://auth.example.com/token".to_string(),
476                        refresh_url: None,
477                        scopes: [("read".to_string(), "Read access".to_string())]
478                            .into_iter()
479                            .collect(),
480                        pkce_required: Some(true),
481                    }),
482                    client_credentials: None,
483                    implicit: None,
484                    password: None,
485                    device_code: None,
486                },
487                description: None,
488                oauth2_metadata_url: None,
489            }),
490            open_id_connect: None,
491            mtls: None,
492        };
493        let json = serde_json::to_string(&scheme).unwrap();
494        let back: SecurityScheme = serde_json::from_str(&json).unwrap();
495        assert_eq!(back, scheme);
496    }
497
498    // --- AgentInterface ---
499
500    #[test]
501    fn agent_interface_json_uses_camel_case() {
502        let iface = AgentInterface {
503            url: "https://example.com".to_string(),
504            protocol_binding: "JSONRPC".to_string(),
505            tenant: None,
506            protocol_version: "0.3".to_string(),
507        };
508        let json = serde_json::to_string(&iface).unwrap();
509        assert!(json.contains("protocolBinding"));
510        assert!(json.contains("protocolVersion"));
511        assert!(!json.contains("protocol_binding"));
512    }
513
514    // --- AgentProvider ---
515
516    #[test]
517    fn agent_provider_roundtrip() {
518        let p = AgentProvider {
519            url: "https://acme.io".to_string(),
520            organization: "ACME Corp".to_string(),
521        };
522        let json = serde_json::to_string(&p).unwrap();
523        let back: AgentProvider = serde_json::from_str(&json).unwrap();
524        assert_eq!(back.organization, "ACME Corp");
525    }
526}