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