1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentCard {
9 pub name: String,
11 pub description: String,
13 pub version: String,
15 #[serde(rename = "supportedInterfaces")]
17 pub supported_interfaces: Vec<AgentInterface>,
18 pub capabilities: AgentCapabilities,
20 pub skills: Vec<AgentSkill>,
22 #[serde(rename = "defaultInputModes")]
24 pub default_input_modes: Vec<String>,
25 #[serde(rename = "defaultOutputModes")]
27 pub default_output_modes: Vec<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub provider: Option<AgentProvider>,
31 #[serde(rename = "securitySchemes", skip_serializing_if = "Option::is_none")]
33 pub security_schemes: Option<HashMap<String, SecurityScheme>>,
34 #[serde(
36 rename = "securityRequirements",
37 skip_serializing_if = "Option::is_none"
38 )]
39 pub security_requirements: Option<Vec<SecurityRequirement>>,
40 #[serde(rename = "documentationUrl", skip_serializing_if = "Option::is_none")]
42 pub documentation_url: Option<String>,
43 #[serde(rename = "iconUrl", skip_serializing_if = "Option::is_none")]
45 pub icon_url: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub signatures: Option<Vec<AgentCardSignature>>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AgentInterface {
54 pub url: String,
56 #[serde(rename = "protocolBinding")]
58 pub protocol_binding: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub tenant: Option<String>,
62 #[serde(rename = "protocolVersion")]
64 pub protocol_version: String,
65}
66
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69pub struct AgentCapabilities {
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub streaming: Option<bool>,
73 #[serde(rename = "pushNotifications", skip_serializing_if = "Option::is_none")]
75 pub push_notifications: Option<bool>,
76 #[serde(rename = "extendedAgentCard", skip_serializing_if = "Option::is_none")]
78 pub extended_agent_card: Option<bool>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub extensions: Option<Vec<AgentExtension>>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AgentExtension {
87 pub uri: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub description: Option<String>,
92 #[serde(default)]
94 pub required: bool,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub params: Option<HashMap<String, serde_json::Value>>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct AgentSkill {
103 pub id: String,
105 pub name: String,
107 pub description: String,
109 pub tags: Vec<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub examples: Option<Vec<String>>,
114 #[serde(rename = "inputModes", skip_serializing_if = "Option::is_none")]
116 pub input_modes: Option<Vec<String>>,
117 #[serde(rename = "outputModes", skip_serializing_if = "Option::is_none")]
119 pub output_modes: Option<Vec<String>>,
120 #[serde(
122 rename = "securityRequirements",
123 skip_serializing_if = "Option::is_none"
124 )]
125 pub security_requirements: Option<Vec<SecurityRequirement>>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct AgentProvider {
131 pub url: String,
133 pub organization: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AgentCardSignature {
140 pub protected: String,
142 pub signature: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub header: Option<HashMap<String, serde_json::Value>>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct SecurityRequirement {
152 pub schemes: HashMap<String, Vec<String>>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct SecurityScheme {
159 #[serde(
161 skip_serializing_if = "Option::is_none",
162 rename = "apiKeySecurityScheme"
163 )]
164 pub api_key: Option<ApiKeySecurityScheme>,
165 #[serde(
167 skip_serializing_if = "Option::is_none",
168 rename = "httpAuthSecurityScheme"
169 )]
170 pub http_auth: Option<HttpAuthSecurityScheme>,
171 #[serde(
173 skip_serializing_if = "Option::is_none",
174 rename = "oauth2SecurityScheme"
175 )]
176 pub oauth2: Option<OAuth2SecurityScheme>,
177 #[serde(
179 skip_serializing_if = "Option::is_none",
180 rename = "openIdConnectSecurityScheme"
181 )]
182 pub open_id_connect: Option<OpenIdConnectSecurityScheme>,
183 #[serde(skip_serializing_if = "Option::is_none", rename = "mtlsSecurityScheme")]
185 pub mtls: Option<MutualTlsSecurityScheme>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct ApiKeySecurityScheme {
191 pub name: String,
193 #[serde(rename = "in")]
195 pub location: String,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub description: Option<String>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
203pub struct HttpAuthSecurityScheme {
204 pub scheme: String,
206 #[serde(rename = "bearerFormat", skip_serializing_if = "Option::is_none")]
208 pub bearer_format: Option<String>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub description: Option<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct OAuth2SecurityScheme {
217 pub flows: OAuthFlows,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub description: Option<String>,
222 #[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
224 pub oauth2_metadata_url: Option<String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229pub struct OpenIdConnectSecurityScheme {
230 #[serde(rename = "openIdConnectUrl")]
232 pub open_id_connect_url: String,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub description: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
240pub struct MutualTlsSecurityScheme {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub description: Option<String>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct OAuthFlows {
249 #[serde(skip_serializing_if = "Option::is_none", rename = "authorizationCode")]
251 pub authorization_code: Option<AuthorizationCodeOAuthFlow>,
252 #[serde(skip_serializing_if = "Option::is_none", rename = "clientCredentials")]
254 pub client_credentials: Option<ClientCredentialsOAuthFlow>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub implicit: Option<ImplicitOAuthFlow>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub password: Option<PasswordOAuthFlow>,
261 #[serde(skip_serializing_if = "Option::is_none", rename = "deviceCode")]
263 pub device_code: Option<DeviceCodeOAuthFlow>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct AuthorizationCodeOAuthFlow {
269 #[serde(rename = "authorizationUrl")]
271 pub authorization_url: String,
272 #[serde(rename = "tokenUrl")]
274 pub token_url: String,
275 #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
277 pub refresh_url: Option<String>,
278 pub scopes: HashMap<String, String>,
280 #[serde(rename = "pkceRequired", skip_serializing_if = "Option::is_none")]
282 pub pkce_required: Option<bool>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct ClientCredentialsOAuthFlow {
288 #[serde(rename = "tokenUrl")]
290 pub token_url: String,
291 #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
293 pub refresh_url: Option<String>,
294 pub scopes: HashMap<String, String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct ImplicitOAuthFlow {
301 #[serde(rename = "authorizationUrl", skip_serializing_if = "Option::is_none")]
303 pub authorization_url: Option<String>,
304 #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
306 pub refresh_url: Option<String>,
307 #[serde(default)]
309 pub scopes: HashMap<String, String>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314pub struct PasswordOAuthFlow {
315 #[serde(rename = "tokenUrl", skip_serializing_if = "Option::is_none")]
317 pub token_url: Option<String>,
318 #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
320 pub refresh_url: Option<String>,
321 #[serde(default)]
323 pub scopes: HashMap<String, String>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328pub struct DeviceCodeOAuthFlow {
329 #[serde(rename = "deviceAuthorizationUrl")]
331 pub device_authorization_url: String,
332 #[serde(rename = "tokenUrl")]
334 pub token_url: String,
335 #[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
337 pub refresh_url: Option<String>,
338 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 #[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 #[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 #[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 #[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 #[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}