1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::Value;
5use std::collections::HashMap;
6
7use crate::types::{ProtocolVersion, TRANSPORT_PROTOCOL_GRPC, TransportProtocol};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct AgentCard {
17 pub name: String,
18 pub description: String,
19 pub version: String,
20 pub supported_interfaces: Vec<AgentInterface>,
21 pub capabilities: AgentCapabilities,
22 pub default_input_modes: Vec<String>,
23 pub default_output_modes: Vec<String>,
24 #[serde(default, deserialize_with = "deserialize_vec_null_as_default")]
25 pub skills: Vec<AgentSkill>,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub provider: Option<AgentProvider>,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub documentation_url: Option<String>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub icon_url: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub security_schemes: Option<HashMap<String, SecurityScheme>>,
38
39 #[serde(
40 default,
41 skip_serializing_if = "Option::is_none",
42 deserialize_with = "deserialize_optional_security_requirements"
43 )]
44 pub security_requirements: Option<Vec<SecurityRequirement>>,
45
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub signatures: Option<Vec<AgentCardSignature>>,
48}
49
50fn deserialize_vec_null_as_default<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
51where
52 D: Deserializer<'de>,
53 T: Deserialize<'de>,
54{
55 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
56}
57
58fn deserialize_optional_security_requirements<'de, D>(
59 deserializer: D,
60) -> Result<Option<Vec<SecurityRequirement>>, D::Error>
61where
62 D: Deserializer<'de>,
63{
64 let raw = Option::<Vec<Value>>::deserialize(deserializer)?;
65
66 raw.map(|items| {
67 items
68 .into_iter()
69 .map(parse_security_requirement_value)
70 .collect()
71 })
72 .transpose()
73}
74
75fn parse_security_requirement_value<E>(value: Value) -> Result<SecurityRequirement, E>
76where
77 E: serde::de::Error,
78{
79 if let Ok(requirement) = serde_json::from_value::<SecurityRequirement>(value.clone()) {
80 return Ok(requirement);
81 }
82
83 let Value::Object(mut object) = value else {
84 return Err(E::custom("security requirement must be an object"));
85 };
86
87 if let Some(schemes) = object.remove("schemes") {
88 return parse_security_requirement_map::<E>(schemes);
89 }
90
91 Err(E::custom("invalid security requirement shape"))
92}
93
94fn parse_security_requirement_map<E>(value: Value) -> Result<SecurityRequirement, E>
95where
96 E: serde::de::Error,
97{
98 let Value::Object(object) = value else {
99 return Err(E::custom("security requirement schemes must be an object"));
100 };
101
102 let mut requirement = HashMap::new();
103 for (scheme, scopes_value) in object {
104 let scopes = match scopes_value {
105 Value::Array(_) => serde_json::from_value::<Vec<String>>(scopes_value)
106 .map_err(|e| E::custom(format!("invalid security scopes for {scheme}: {e}")))?,
107 Value::Object(mut wrapped) => {
108 let Some(list) = wrapped.remove("list") else {
109 return Err(E::custom(format!(
110 "invalid wrapped security scopes for {scheme}"
111 )));
112 };
113 serde_json::from_value::<Vec<String>>(list).map_err(|e| {
114 E::custom(format!("invalid wrapped security scopes for {scheme}: {e}"))
115 })?
116 }
117 _ => {
118 return Err(E::custom(format!(
119 "security scopes for {scheme} must be a list"
120 )));
121 }
122 };
123
124 requirement.insert(scheme, scopes);
125 }
126
127 Ok(requirement)
128}
129
130#[derive(Debug, Clone, PartialEq)]
136pub struct AgentInterface {
137 pub url: String,
138 pub protocol_binding: TransportProtocol,
139 pub protocol_version: ProtocolVersion,
140 pub tenant: Option<String>,
141}
142
143#[derive(Deserialize)]
144#[serde(rename_all = "camelCase")]
145struct AgentInterfaceSerde {
146 url: String,
147 protocol_binding: TransportProtocol,
148 protocol_version: ProtocolVersion,
149 #[serde(default)]
150 tenant: Option<String>,
151}
152
153fn normalize_agent_interface_url(url: String, protocol_binding: &str) -> String {
154 if protocol_binding.eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC) {
155 if let Some(stripped) = url.strip_prefix("http://") {
156 return stripped.to_string();
157 }
158 }
159
160 url
161}
162
163impl AgentInterface {
164 pub fn new(url: impl Into<String>, protocol_binding: impl Into<String>) -> Self {
165 let protocol_binding = protocol_binding.into();
166 AgentInterface {
167 url: normalize_agent_interface_url(url.into(), &protocol_binding),
168 protocol_binding,
169 protocol_version: crate::VERSION.to_string(),
170 tenant: None,
171 }
172 }
173
174 pub fn wire_url(&self) -> String {
175 normalize_agent_interface_url(self.url.clone(), &self.protocol_binding)
176 }
177}
178
179impl Serialize for AgentInterface {
180 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
181 use serde::ser::SerializeStruct;
182
183 let mut state = serializer
184 .serialize_struct("AgentInterface", if self.tenant.is_some() { 4 } else { 3 })?;
185 state.serialize_field("url", &self.wire_url())?;
186 state.serialize_field("protocolBinding", &self.protocol_binding)?;
187 state.serialize_field("protocolVersion", &self.protocol_version)?;
188 if let Some(tenant) = &self.tenant {
189 state.serialize_field("tenant", tenant)?;
190 }
191 state.end()
192 }
193}
194
195impl<'de> Deserialize<'de> for AgentInterface {
196 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
197 let raw = AgentInterfaceSerde::deserialize(deserializer)?;
198
199 Ok(Self {
200 url: normalize_agent_interface_url(raw.url, &raw.protocol_binding),
201 protocol_binding: raw.protocol_binding,
202 protocol_version: raw.protocol_version,
203 tenant: raw.tenant,
204 })
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct AgentProvider {
216 pub organization: String,
217 pub url: String,
218}
219
220#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct AgentCapabilities {
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub streaming: Option<bool>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub push_notifications: Option<bool>,
233
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub extensions: Option<Vec<AgentExtension>>,
236
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub extended_agent_card: Option<bool>,
239}
240
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct AgentExtension {
249 pub uri: String,
250
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub description: Option<String>,
253
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub required: Option<bool>,
256
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub params: Option<HashMap<String, Value>>,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct AgentSkill {
269 pub id: String,
270 pub name: String,
271 pub description: String,
272 pub tags: Vec<String>,
273
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub examples: Option<Vec<String>>,
276
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub input_modes: Option<Vec<String>>,
279
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub output_modes: Option<Vec<String>>,
282
283 #[serde(
284 default,
285 skip_serializing_if = "Option::is_none",
286 deserialize_with = "deserialize_optional_security_requirements"
287 )]
288 pub security_requirements: Option<Vec<SecurityRequirement>>,
289}
290
291#[derive(Debug, Clone, PartialEq)]
297pub enum SecurityScheme {
298 ApiKey(ApiKeySecurityScheme),
299 HttpAuth(HttpAuthSecurityScheme),
300 OAuth2(OAuth2SecurityScheme),
301 OpenIdConnect(OpenIdConnectSecurityScheme),
302 MutualTls(MutualTlsSecurityScheme),
303}
304
305impl Serialize for SecurityScheme {
306 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
307 use serde::ser::SerializeMap;
308 let mut map = serializer.serialize_map(Some(1))?;
309 match self {
310 SecurityScheme::ApiKey(s) => map.serialize_entry("apiKeySecurityScheme", s)?,
311 SecurityScheme::HttpAuth(s) => map.serialize_entry("httpAuthSecurityScheme", s)?,
312 SecurityScheme::OAuth2(s) => map.serialize_entry("oauth2SecurityScheme", s)?,
313 SecurityScheme::OpenIdConnect(s) => {
314 map.serialize_entry("openIdConnectSecurityScheme", s)?
315 }
316 SecurityScheme::MutualTls(s) => map.serialize_entry("mtlsSecurityScheme", s)?,
317 }
318 map.end()
319 }
320}
321
322impl<'de> Deserialize<'de> for SecurityScheme {
323 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
324 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
325 if let Some(v) = raw.get("apiKeySecurityScheme") {
326 Ok(SecurityScheme::ApiKey(
327 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
328 ))
329 } else if let Some(v) = raw.get("httpAuthSecurityScheme") {
330 Ok(SecurityScheme::HttpAuth(
331 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
332 ))
333 } else if let Some(v) = raw.get("oauth2SecurityScheme") {
334 Ok(SecurityScheme::OAuth2(
335 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
336 ))
337 } else if let Some(v) = raw.get("openIdConnectSecurityScheme") {
338 Ok(SecurityScheme::OpenIdConnect(
339 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
340 ))
341 } else if let Some(v) = raw.get("mtlsSecurityScheme") {
342 Ok(SecurityScheme::MutualTls(
343 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
344 ))
345 } else {
346 Err(serde::de::Error::custom("unknown security scheme variant"))
347 }
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct ApiKeySecurityScheme {
358 pub location: String,
359 pub name: String,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub description: Option<String>,
362}
363
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct HttpAuthSecurityScheme {
367 pub scheme: String,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub description: Option<String>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub bearer_format: Option<String>,
372}
373
374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct OAuth2SecurityScheme {
377 pub flows: OAuthFlows,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub description: Option<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub oauth2_metadata_url: Option<String>,
382}
383
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct OpenIdConnectSecurityScheme {
387 pub open_id_connect_url: String,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub description: Option<String>,
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct MutualTlsSecurityScheme {
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub description: Option<String>,
397}
398
399#[derive(Debug, Clone, PartialEq)]
404pub enum OAuthFlows {
405 AuthorizationCode(AuthorizationCodeOAuthFlow),
406 ClientCredentials(ClientCredentialsOAuthFlow),
407 DeviceCode(DeviceCodeOAuthFlow),
408 Implicit(ImplicitOAuthFlow),
409 Password(PasswordOAuthFlow),
410}
411
412impl Serialize for OAuthFlows {
413 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
414 use serde::ser::SerializeMap;
415 let mut map = serializer.serialize_map(Some(1))?;
416 match self {
417 OAuthFlows::AuthorizationCode(f) => map.serialize_entry("authorizationCode", f)?,
418 OAuthFlows::ClientCredentials(f) => map.serialize_entry("clientCredentials", f)?,
419 OAuthFlows::DeviceCode(f) => map.serialize_entry("deviceCode", f)?,
420 OAuthFlows::Implicit(f) => map.serialize_entry("implicit", f)?,
421 OAuthFlows::Password(f) => map.serialize_entry("password", f)?,
422 }
423 map.end()
424 }
425}
426
427impl<'de> Deserialize<'de> for OAuthFlows {
428 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
429 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
430 if let Some(v) = raw.get("authorizationCode") {
431 Ok(OAuthFlows::AuthorizationCode(
432 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
433 ))
434 } else if let Some(v) = raw.get("clientCredentials") {
435 Ok(OAuthFlows::ClientCredentials(
436 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
437 ))
438 } else if let Some(v) = raw.get("deviceCode") {
439 Ok(OAuthFlows::DeviceCode(
440 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
441 ))
442 } else if let Some(v) = raw.get("implicit") {
443 Ok(OAuthFlows::Implicit(
444 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
445 ))
446 } else if let Some(v) = raw.get("password") {
447 Ok(OAuthFlows::Password(
448 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
449 ))
450 } else {
451 Err(serde::de::Error::custom("unknown OAuth flow variant"))
452 }
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct AuthorizationCodeOAuthFlow {
459 pub authorization_url: String,
460 pub token_url: String,
461 pub scopes: HashMap<String, String>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub refresh_url: Option<String>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub pkce_required: Option<bool>,
466}
467
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct ClientCredentialsOAuthFlow {
471 pub token_url: String,
472 pub scopes: HashMap<String, String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub refresh_url: Option<String>,
475}
476
477#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct DeviceCodeOAuthFlow {
480 pub device_authorization_url: String,
481 pub token_url: String,
482 pub scopes: HashMap<String, String>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub refresh_url: Option<String>,
485}
486
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct ImplicitOAuthFlow {
490 pub authorization_url: String,
491 pub scopes: HashMap<String, String>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub refresh_url: Option<String>,
494}
495
496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct PasswordOAuthFlow {
499 pub token_url: String,
500 pub scopes: HashMap<String, String>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub refresh_url: Option<String>,
503}
504
505pub type SecurityRequirement = HashMap<String, Vec<String>>;
511
512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct AgentCardSignature {
520 pub protected: String,
521 pub signature: String,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub header: Option<HashMap<String, Value>>,
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_agent_card_serde() {
532 let card = AgentCard {
533 name: "Test Agent".to_string(),
534 description: "A test agent".to_string(),
535 version: "1.0.0".to_string(),
536 supported_interfaces: vec![AgentInterface::new("http://localhost:3000", "JSONRPC")],
537 capabilities: AgentCapabilities {
538 streaming: Some(true),
539 push_notifications: Some(false),
540 extensions: None,
541 extended_agent_card: None,
542 },
543 default_input_modes: vec!["text/plain".to_string()],
544 default_output_modes: vec!["text/plain".to_string()],
545 skills: vec![AgentSkill {
546 id: "echo".to_string(),
547 name: "Echo".to_string(),
548 description: "Echoes input".to_string(),
549 tags: vec!["test".to_string()],
550 examples: Some(vec!["hello".to_string()]),
551 input_modes: None,
552 output_modes: None,
553 security_requirements: None,
554 }],
555 provider: Some(AgentProvider {
556 organization: "Test Corp".to_string(),
557 url: "https://test.com".to_string(),
558 }),
559 documentation_url: None,
560 icon_url: None,
561 security_schemes: None,
562 security_requirements: None,
563 signatures: None,
564 };
565
566 let json = serde_json::to_string(&card).unwrap();
567 let back: AgentCard = serde_json::from_str(&json).unwrap();
568 assert_eq!(card, back);
569 }
570
571 #[test]
572 fn test_agent_interface_new() {
573 let iface = AgentInterface::new("http://localhost:3000", "JSONRPC");
574 assert_eq!(iface.url, "http://localhost:3000");
575 assert_eq!(iface.protocol_binding, "JSONRPC");
576 assert!(!iface.protocol_version.is_empty());
577 }
578
579 #[test]
580 fn test_agent_interface_new_normalizes_grpc_http_scheme() {
581 let iface = AgentInterface::new("http://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
582 assert_eq!(iface.url, "localhost:50051");
583 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
584 }
585
586 #[test]
587 fn test_agent_interface_new_preserves_grpc_https_scheme() {
588 let iface = AgentInterface::new("https://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
589 assert_eq!(iface.url, "https://localhost:50051");
590 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
591 }
592
593 #[test]
594 fn test_agent_interface_serde_normalizes_grpc_http_scheme() {
595 let iface = AgentInterface {
596 url: "http://localhost:50051".to_string(),
597 protocol_binding: TRANSPORT_PROTOCOL_GRPC.to_string(),
598 protocol_version: crate::VERSION.to_string(),
599 tenant: Some("tenant-a".to_string()),
600 };
601
602 let json = serde_json::to_string(&iface).unwrap();
603 assert!(json.contains("\"url\":\"localhost:50051\""));
604
605 let back: AgentInterface = serde_json::from_str(&json).unwrap();
606 assert_eq!(back.url, "localhost:50051");
607 assert_eq!(back.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
608 assert_eq!(back.tenant.as_deref(), Some("tenant-a"));
609 }
610
611 #[test]
612 fn test_agent_card_deserialize_null_skills_as_default() {
613 let json = serde_json::json!({
614 "name": "Test Agent",
615 "description": "A test agent",
616 "version": "1.0.0",
617 "supportedInterfaces": [
618 {
619 "url": "http://localhost:3000",
620 "protocolBinding": "JSONRPC",
621 "protocolVersion": crate::VERSION
622 }
623 ],
624 "capabilities": {},
625 "defaultInputModes": ["text/plain"],
626 "defaultOutputModes": ["text/plain"],
627 "skills": null
628 });
629
630 let card: AgentCard = serde_json::from_value(json).unwrap();
631 assert!(card.skills.is_empty());
632 }
633
634 #[test]
635 fn test_security_scheme_deserialize_unknown_variant_errors() {
636 let err = serde_json::from_value::<SecurityScheme>(serde_json::json!({
637 "unknown": {"value": true}
638 }))
639 .unwrap_err();
640
641 assert!(err.to_string().contains("unknown security scheme variant"));
642 }
643
644 #[test]
645 fn test_oauth_flows_deserialize_unknown_variant_errors() {
646 let err = serde_json::from_value::<OAuthFlows>(serde_json::json!({
647 "unknown": {"tokenUrl": "https://example.com/token"}
648 }))
649 .unwrap_err();
650
651 assert!(err.to_string().contains("unknown OAuth flow variant"));
652 }
653
654 #[test]
655 fn test_security_scheme_apikey_serde() {
656 let ss = SecurityScheme::ApiKey(ApiKeySecurityScheme {
657 location: "header".to_string(),
658 name: "X-API-Key".to_string(),
659 description: None,
660 });
661 let json = serde_json::to_string(&ss).unwrap();
662 assert!(json.contains("apiKeySecurityScheme"));
663 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
664 assert_eq!(ss, back);
665 }
666
667 #[test]
668 fn test_security_scheme_httpauth_serde() {
669 let ss = SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
670 scheme: "Bearer".to_string(),
671 description: None,
672 bearer_format: Some("JWT".to_string()),
673 });
674 let json = serde_json::to_string(&ss).unwrap();
675 assert!(json.contains("httpAuthSecurityScheme"));
676 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
677 assert_eq!(ss, back);
678 }
679
680 #[test]
681 fn test_security_scheme_oauth2_serde() {
682 let ss = SecurityScheme::OAuth2(OAuth2SecurityScheme {
683 flows: OAuthFlows::ClientCredentials(ClientCredentialsOAuthFlow {
684 token_url: "https://auth.example.com/token".to_string(),
685 scopes: [("read".to_string(), "Read access".to_string())]
686 .into_iter()
687 .collect(),
688 refresh_url: None,
689 }),
690 description: None,
691 oauth2_metadata_url: None,
692 });
693 let json = serde_json::to_string(&ss).unwrap();
694 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
695 assert_eq!(ss, back);
696 }
697
698 #[test]
699 fn test_security_scheme_openidconnect_serde() {
700 let ss = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
701 open_id_connect_url: "https://example.com/.well-known/openid-configuration".to_string(),
702 description: None,
703 });
704 let json = serde_json::to_string(&ss).unwrap();
705 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
706 assert_eq!(ss, back);
707 }
708
709 #[test]
710 fn test_security_scheme_mtls_serde() {
711 let ss = SecurityScheme::MutualTls(MutualTlsSecurityScheme {
712 description: Some("mTLS auth".to_string()),
713 });
714 let json = serde_json::to_string(&ss).unwrap();
715 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
716 assert_eq!(ss, back);
717 }
718
719 #[test]
720 fn test_oauth_flows_all_variants() {
721 let flows = [
722 OAuthFlows::AuthorizationCode(AuthorizationCodeOAuthFlow {
723 authorization_url: "https://auth.example.com/authorize".to_string(),
724 token_url: "https://auth.example.com/token".to_string(),
725 scopes: HashMap::new(),
726 refresh_url: None,
727 pkce_required: Some(true),
728 }),
729 OAuthFlows::DeviceCode(DeviceCodeOAuthFlow {
730 device_authorization_url: "https://auth.example.com/device".to_string(),
731 token_url: "https://auth.example.com/token".to_string(),
732 scopes: HashMap::new(),
733 refresh_url: None,
734 }),
735 OAuthFlows::Implicit(ImplicitOAuthFlow {
736 authorization_url: "https://auth.example.com/authorize".to_string(),
737 scopes: HashMap::new(),
738 refresh_url: None,
739 }),
740 OAuthFlows::Password(PasswordOAuthFlow {
741 token_url: "https://auth.example.com/token".to_string(),
742 scopes: HashMap::new(),
743 refresh_url: None,
744 }),
745 ];
746 for flow in flows {
747 let json = serde_json::to_string(&flow).unwrap();
748 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
749 assert_eq!(flow, back);
750 }
751 }
752
753 #[test]
754 fn test_agent_capabilities_default() {
755 let caps = AgentCapabilities::default();
756 assert_eq!(caps.streaming, None);
757 assert_eq!(caps.push_notifications, None);
758 assert_eq!(caps.extensions, None);
759 assert_eq!(caps.extended_agent_card, None);
760 }
761
762 #[test]
763 fn test_agent_card_with_security_schemes() {
764 let mut schemes = HashMap::new();
765 schemes.insert(
766 "bearer".to_string(),
767 SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
768 scheme: "Bearer".to_string(),
769 description: None,
770 bearer_format: None,
771 }),
772 );
773 let card = AgentCard {
774 name: "Secure Agent".to_string(),
775 description: "Agent with auth".to_string(),
776 version: "1.0.0".to_string(),
777 supported_interfaces: vec![],
778 capabilities: AgentCapabilities::default(),
779 default_input_modes: vec![],
780 default_output_modes: vec![],
781 skills: vec![],
782 provider: None,
783 documentation_url: None,
784 icon_url: None,
785 security_schemes: Some(schemes),
786 security_requirements: Some(vec![
787 [("bearer".to_string(), vec![])].into_iter().collect(),
788 ]),
789 signatures: None,
790 };
791 let json = serde_json::to_string(&card).unwrap();
792 let back: AgentCard = serde_json::from_str(&json).unwrap();
793 assert_eq!(card, back);
794 }
795
796 #[test]
797 fn test_agent_card_deserializes_null_skills_as_empty() {
798 let card: AgentCard = serde_json::from_str(
799 r#"{
800 "name": "Test Agent",
801 "description": "A test agent",
802 "version": "1.0.0",
803 "supportedInterfaces": [
804 {
805 "url": "http://localhost:3000",
806 "protocolBinding": "JSONRPC",
807 "protocolVersion": "1.0"
808 }
809 ],
810 "capabilities": {
811 "streaming": true
812 },
813 "defaultInputModes": ["text/plain"],
814 "defaultOutputModes": ["text/plain"],
815 "skills": null
816 }"#,
817 )
818 .unwrap();
819
820 assert!(card.skills.is_empty());
821 }
822
823 #[test]
824 fn test_agent_card_deserializes_missing_skills_as_empty() {
825 let card: AgentCard = serde_json::from_str(
826 r#"{
827 "name": "Test Agent",
828 "description": "A test agent",
829 "version": "1.0.0",
830 "supportedInterfaces": [
831 {
832 "url": "http://localhost:3000",
833 "protocolBinding": "JSONRPC",
834 "protocolVersion": "1.0"
835 }
836 ],
837 "capabilities": {
838 "streaming": true
839 },
840 "defaultInputModes": ["text/plain"],
841 "defaultOutputModes": ["text/plain"]
842 }"#,
843 )
844 .unwrap();
845
846 assert!(card.skills.is_empty());
847 }
848
849 #[test]
850 fn test_agent_card_deserializes_wrapped_security_requirements() {
851 let card: AgentCard = serde_json::from_str(
852 r#"{
853 "name": "Spec Agent",
854 "description": "A test agent",
855 "version": "1.0.0",
856 "supportedInterfaces": [
857 {
858 "url": "https://example.com/spec",
859 "protocolBinding": "JSONRPC",
860 "protocolVersion": "1.0"
861 }
862 ],
863 "capabilities": {
864 "streaming": true
865 },
866 "defaultInputModes": ["text/plain"],
867 "defaultOutputModes": ["text/plain"],
868 "skills": [],
869 "securityRequirements": [
870 {
871 "schemes": {
872 "bearer_token": {
873 "list": []
874 }
875 }
876 }
877 ]
878 }"#,
879 )
880 .unwrap();
881
882 let requirements = card.security_requirements.unwrap();
883 assert_eq!(requirements.len(), 1);
884 assert_eq!(requirements[0].get("bearer_token"), Some(&Vec::new()));
885 }
886}