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 serialize_with = "serialize_optional_security_requirements",
48 deserialize_with = "deserialize_optional_security_requirements"
49 )]
50 pub security_requirements: Option<Vec<SecurityRequirement>>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub signatures: Option<Vec<AgentCardSignature>>,
54}
55
56fn deserialize_vec_null_as_default<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
57where
58 D: Deserializer<'de>,
59 T: Deserialize<'de>,
60{
61 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
62}
63
64fn deserialize_optional_security_requirements<'de, D>(
65 deserializer: D,
66) -> Result<Option<Vec<SecurityRequirement>>, D::Error>
67where
68 D: Deserializer<'de>,
69{
70 let raw = Option::<Vec<Value>>::deserialize(deserializer)?;
71
72 raw.map(|items| {
73 items
74 .into_iter()
75 .map(parse_security_requirement_value)
76 .collect()
77 })
78 .transpose()
79}
80
81fn serialize_optional_security_requirements<S>(
82 requirements: &Option<Vec<SecurityRequirement>>,
83 serializer: S,
84) -> Result<S::Ok, S::Error>
85where
86 S: Serializer,
87{
88 match requirements {
89 Some(requirements) => requirements
90 .iter()
91 .map(|requirement| {
92 let schemes = requirement
93 .iter()
94 .map(|(scheme, scopes)| {
95 (scheme.as_str(), SerializedSecurityScopes { list: scopes })
96 })
97 .collect();
98 SerializedSecurityRequirement { schemes }
99 })
100 .collect::<Vec<_>>()
101 .serialize(serializer),
102 None => serializer.serialize_none(),
103 }
104}
105
106#[derive(Serialize)]
107struct SerializedSecurityRequirement<'a> {
108 schemes: HashMap<&'a str, SerializedSecurityScopes<'a>>,
109}
110
111#[derive(Serialize)]
112struct SerializedSecurityScopes<'a> {
113 list: &'a [String],
114}
115
116fn parse_security_requirement_value<E>(value: Value) -> Result<SecurityRequirement, E>
117where
118 E: serde::de::Error,
119{
120 if let Ok(requirement) = serde_json::from_value::<SecurityRequirement>(value.clone()) {
121 return Ok(requirement);
122 }
123
124 let Value::Object(mut object) = value else {
125 return Err(E::custom("security requirement must be an object"));
126 };
127
128 if let Some(schemes) = object.remove("schemes") {
129 return parse_security_requirement_map::<E>(schemes);
130 }
131
132 Err(E::custom("invalid security requirement shape"))
133}
134
135fn parse_security_requirement_map<E>(value: Value) -> Result<SecurityRequirement, E>
136where
137 E: serde::de::Error,
138{
139 let Value::Object(object) = value else {
140 return Err(E::custom("security requirement schemes must be an object"));
141 };
142
143 let mut requirement = HashMap::new();
144 for (scheme, scopes_value) in object {
145 let scopes = match scopes_value {
146 Value::Array(_) => serde_json::from_value::<Vec<String>>(scopes_value)
147 .map_err(|e| E::custom(format!("invalid security scopes for {scheme}: {e}")))?,
148 Value::Object(mut wrapped) => match wrapped.remove("list") {
149 Some(list) => serde_json::from_value::<Vec<String>>(list).map_err(|e| {
150 E::custom(format!("invalid wrapped security scopes for {scheme}: {e}"))
151 })?,
152 None if wrapped.is_empty() => Vec::new(),
154 None => {
155 return Err(E::custom(format!(
156 "invalid wrapped security scopes for {scheme}"
157 )));
158 }
159 },
160 _ => {
161 return Err(E::custom(format!(
162 "security scopes for {scheme} must be a list"
163 )));
164 }
165 };
166
167 requirement.insert(scheme, scopes);
168 }
169
170 Ok(requirement)
171}
172
173#[derive(Debug, Clone, PartialEq)]
179pub struct AgentInterface {
180 pub url: String,
181 pub protocol_binding: TransportProtocol,
182 pub protocol_version: ProtocolVersion,
183 pub tenant: Option<String>,
184}
185
186#[cfg(feature = "fuzzing")]
193impl<'a> arbitrary::Arbitrary<'a> for AgentInterface {
194 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
195 let url = String::arbitrary(u)?;
196 let protocol_binding = TransportProtocol::arbitrary(u)?;
197 Ok(AgentInterface {
198 url: normalize_agent_interface_url(url, &protocol_binding),
199 protocol_binding,
200 protocol_version: ProtocolVersion::arbitrary(u)?,
201 tenant: Option::<String>::arbitrary(u)?,
202 })
203 }
204}
205
206#[derive(Deserialize)]
207#[serde(rename_all = "camelCase")]
208struct AgentInterfaceSerde {
209 url: String,
210 protocol_binding: TransportProtocol,
211 protocol_version: ProtocolVersion,
212 #[serde(default)]
213 tenant: Option<String>,
214}
215
216fn normalize_agent_interface_url(url: String, protocol_binding: &str) -> String {
217 if protocol_binding.eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC) {
218 if let Some(stripped) = url.strip_prefix("http://") {
219 return stripped.to_string();
220 }
221 }
222
223 url
224}
225
226impl AgentInterface {
227 pub fn new(url: impl Into<String>, protocol_binding: impl Into<String>) -> Self {
228 let protocol_binding = protocol_binding.into();
229 AgentInterface {
230 url: normalize_agent_interface_url(url.into(), &protocol_binding),
231 protocol_binding,
232 protocol_version: crate::VERSION.to_string(),
233 tenant: None,
234 }
235 }
236
237 pub fn wire_url(&self) -> String {
238 normalize_agent_interface_url(self.url.clone(), &self.protocol_binding)
239 }
240}
241
242impl Serialize for AgentInterface {
243 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
244 use serde::ser::SerializeStruct;
245
246 let mut state = serializer
247 .serialize_struct("AgentInterface", if self.tenant.is_some() { 4 } else { 3 })?;
248 state.serialize_field("url", &self.wire_url())?;
249 state.serialize_field("protocolBinding", &self.protocol_binding)?;
250 state.serialize_field("protocolVersion", &self.protocol_version)?;
251 if let Some(tenant) = &self.tenant {
252 state.serialize_field("tenant", tenant)?;
253 }
254 state.end()
255 }
256}
257
258impl<'de> Deserialize<'de> for AgentInterface {
259 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
260 let raw = AgentInterfaceSerde::deserialize(deserializer)?;
261
262 Ok(Self {
263 url: normalize_agent_interface_url(raw.url, &raw.protocol_binding),
264 protocol_binding: raw.protocol_binding,
265 protocol_version: raw.protocol_version,
266 tenant: raw.tenant,
267 })
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
278#[serde(rename_all = "camelCase")]
279pub struct AgentProvider {
280 pub organization: String,
281 pub url: String,
282}
283
284#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
290#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
291#[serde(rename_all = "camelCase")]
292pub struct AgentCapabilities {
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub streaming: Option<bool>,
295
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub push_notifications: Option<bool>,
298
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub extensions: Option<Vec<AgentExtension>>,
301
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub extended_agent_card: Option<bool>,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
313#[serde(rename_all = "camelCase")]
314pub struct AgentExtension {
315 pub uri: String,
316
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub description: Option<String>,
319
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub required: Option<bool>,
322
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 #[cfg_attr(feature = "fuzzing", arbitrary(default))]
325 pub params: Option<HashMap<String, Value>>,
326}
327
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
335#[serde(rename_all = "camelCase")]
336pub struct AgentSkill {
337 pub id: String,
338 pub name: String,
339 pub description: String,
340 pub tags: Vec<String>,
341
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub examples: Option<Vec<String>>,
344
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub input_modes: Option<Vec<String>>,
347
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub output_modes: Option<Vec<String>>,
350
351 #[serde(
352 default,
353 skip_serializing_if = "Option::is_none",
354 serialize_with = "serialize_optional_security_requirements",
355 deserialize_with = "deserialize_optional_security_requirements"
356 )]
357 pub security_requirements: Option<Vec<SecurityRequirement>>,
358}
359
360#[derive(Debug, Clone, PartialEq)]
366#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
367pub enum SecurityScheme {
368 ApiKey(ApiKeySecurityScheme),
369 HttpAuth(HttpAuthSecurityScheme),
370 OAuth2(OAuth2SecurityScheme),
371 OpenIdConnect(OpenIdConnectSecurityScheme),
372 MutualTls(MutualTlsSecurityScheme),
373}
374
375impl Serialize for SecurityScheme {
376 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
377 use serde::ser::SerializeMap;
378 let mut map = serializer.serialize_map(Some(1))?;
379 match self {
380 SecurityScheme::ApiKey(s) => map.serialize_entry("apiKeySecurityScheme", s)?,
381 SecurityScheme::HttpAuth(s) => map.serialize_entry("httpAuthSecurityScheme", s)?,
382 SecurityScheme::OAuth2(s) => map.serialize_entry("oauth2SecurityScheme", s)?,
383 SecurityScheme::OpenIdConnect(s) => {
384 map.serialize_entry("openIdConnectSecurityScheme", s)?
385 }
386 SecurityScheme::MutualTls(s) => map.serialize_entry("mtlsSecurityScheme", s)?,
387 }
388 map.end()
389 }
390}
391
392impl<'de> Deserialize<'de> for SecurityScheme {
393 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
394 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
395 if let Some(v) = raw.get("apiKeySecurityScheme") {
396 Ok(SecurityScheme::ApiKey(
397 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
398 ))
399 } else if let Some(v) = raw.get("httpAuthSecurityScheme") {
400 Ok(SecurityScheme::HttpAuth(
401 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
402 ))
403 } else if let Some(v) = raw.get("oauth2SecurityScheme") {
404 Ok(SecurityScheme::OAuth2(
405 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
406 ))
407 } else if let Some(v) = raw.get("openIdConnectSecurityScheme") {
408 Ok(SecurityScheme::OpenIdConnect(
409 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
410 ))
411 } else if let Some(v) = raw.get("mtlsSecurityScheme") {
412 Ok(SecurityScheme::MutualTls(
413 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
414 ))
415 } else {
416 Err(serde::de::Error::custom("unknown security scheme variant"))
417 }
418 }
419}
420
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
427#[serde(rename_all = "camelCase")]
428pub struct ApiKeySecurityScheme {
429 pub location: String,
430 pub name: String,
431 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub description: Option<String>,
433}
434
435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
436#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
437#[serde(rename_all = "camelCase")]
438pub struct HttpAuthSecurityScheme {
439 pub scheme: String,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub description: Option<String>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub bearer_format: Option<String>,
444}
445
446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
447#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
448#[serde(rename_all = "camelCase")]
449pub struct OAuth2SecurityScheme {
450 pub flows: OAuthFlows,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub description: Option<String>,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub oauth2_metadata_url: Option<String>,
455}
456
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
459#[serde(rename_all = "camelCase")]
460pub struct OpenIdConnectSecurityScheme {
461 pub open_id_connect_url: String,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub description: Option<String>,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
467#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
468#[serde(rename_all = "camelCase")]
469pub struct MutualTlsSecurityScheme {
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub description: Option<String>,
472}
473
474#[derive(Debug, Clone, PartialEq)]
479#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
480pub enum OAuthFlows {
481 AuthorizationCode(AuthorizationCodeOAuthFlow),
482 ClientCredentials(ClientCredentialsOAuthFlow),
483 DeviceCode(DeviceCodeOAuthFlow),
484 Implicit(ImplicitOAuthFlow),
485 Password(PasswordOAuthFlow),
486}
487
488impl Serialize for OAuthFlows {
489 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
490 use serde::ser::SerializeMap;
491 let mut map = serializer.serialize_map(Some(1))?;
492 match self {
493 OAuthFlows::AuthorizationCode(f) => map.serialize_entry("authorizationCode", f)?,
494 OAuthFlows::ClientCredentials(f) => map.serialize_entry("clientCredentials", f)?,
495 OAuthFlows::DeviceCode(f) => map.serialize_entry("deviceCode", f)?,
496 OAuthFlows::Implicit(f) => map.serialize_entry("implicit", f)?,
497 OAuthFlows::Password(f) => map.serialize_entry("password", f)?,
498 }
499 map.end()
500 }
501}
502
503impl<'de> Deserialize<'de> for OAuthFlows {
504 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
505 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
506 if let Some(v) = raw.get("authorizationCode") {
507 Ok(OAuthFlows::AuthorizationCode(
508 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
509 ))
510 } else if let Some(v) = raw.get("clientCredentials") {
511 Ok(OAuthFlows::ClientCredentials(
512 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
513 ))
514 } else if let Some(v) = raw.get("deviceCode") {
515 Ok(OAuthFlows::DeviceCode(
516 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
517 ))
518 } else if let Some(v) = raw.get("implicit") {
519 Ok(OAuthFlows::Implicit(
520 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
521 ))
522 } else if let Some(v) = raw.get("password") {
523 Ok(OAuthFlows::Password(
524 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
525 ))
526 } else {
527 Err(serde::de::Error::custom("unknown OAuth flow variant"))
528 }
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
533#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
534#[serde(rename_all = "camelCase")]
535pub struct AuthorizationCodeOAuthFlow {
536 pub authorization_url: String,
537 pub token_url: String,
538 pub scopes: HashMap<String, String>,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub refresh_url: Option<String>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub pkce_required: Option<bool>,
543}
544
545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
546#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
547#[serde(rename_all = "camelCase")]
548pub struct ClientCredentialsOAuthFlow {
549 pub token_url: String,
550 pub scopes: HashMap<String, String>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub refresh_url: Option<String>,
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
556#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
557#[serde(rename_all = "camelCase")]
558pub struct DeviceCodeOAuthFlow {
559 pub device_authorization_url: String,
560 pub token_url: String,
561 pub scopes: HashMap<String, String>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub refresh_url: Option<String>,
564}
565
566#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
567#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
568#[serde(rename_all = "camelCase")]
569pub struct ImplicitOAuthFlow {
570 pub authorization_url: String,
571 pub scopes: HashMap<String, String>,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub refresh_url: Option<String>,
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
578#[serde(rename_all = "camelCase")]
579pub struct PasswordOAuthFlow {
580 pub token_url: String,
581 pub scopes: HashMap<String, String>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub refresh_url: Option<String>,
584}
585
586pub type SecurityRequirement = HashMap<String, Vec<String>>;
592
593#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
599#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
600#[serde(rename_all = "camelCase")]
601pub struct AgentCardSignature {
602 pub protected: String,
603 pub signature: String,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 #[cfg_attr(feature = "fuzzing", arbitrary(default))]
606 pub header: Option<HashMap<String, Value>>,
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_agent_card_serde() {
615 let card = AgentCard {
616 name: "Test Agent".to_string(),
617 description: "A test agent".to_string(),
618 version: "1.0.0".to_string(),
619 supported_interfaces: vec![AgentInterface::new("http://localhost:3000", "JSONRPC")],
620 capabilities: AgentCapabilities {
621 streaming: Some(true),
622 push_notifications: Some(false),
623 extensions: None,
624 extended_agent_card: None,
625 },
626 default_input_modes: vec!["text/plain".to_string()],
627 default_output_modes: vec!["text/plain".to_string()],
628 skills: vec![AgentSkill {
629 id: "echo".to_string(),
630 name: "Echo".to_string(),
631 description: "Echoes input".to_string(),
632 tags: vec!["test".to_string()],
633 examples: Some(vec!["hello".to_string()]),
634 input_modes: None,
635 output_modes: None,
636 security_requirements: None,
637 }],
638 provider: Some(AgentProvider {
639 organization: "Test Corp".to_string(),
640 url: "https://test.com".to_string(),
641 }),
642 documentation_url: None,
643 icon_url: None,
644 security_schemes: None,
645 security_requirements: None,
646 signatures: None,
647 };
648
649 let json = serde_json::to_string(&card).unwrap();
650 let back: AgentCard = serde_json::from_str(&json).unwrap();
651 assert_eq!(card, back);
652 }
653
654 #[test]
655 fn test_agent_interface_new() {
656 let iface = AgentInterface::new("http://localhost:3000", "JSONRPC");
657 assert_eq!(iface.url, "http://localhost:3000");
658 assert_eq!(iface.protocol_binding, "JSONRPC");
659 assert!(!iface.protocol_version.is_empty());
660 }
661
662 #[test]
663 fn test_agent_interface_new_normalizes_grpc_http_scheme() {
664 let iface = AgentInterface::new("http://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
665 assert_eq!(iface.url, "localhost:50051");
666 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
667 }
668
669 #[test]
670 fn test_agent_interface_new_preserves_grpc_https_scheme() {
671 let iface = AgentInterface::new("https://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
672 assert_eq!(iface.url, "https://localhost:50051");
673 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
674 }
675
676 #[test]
677 fn test_agent_interface_serde_normalizes_grpc_http_scheme() {
678 let iface = AgentInterface {
679 url: "http://localhost:50051".to_string(),
680 protocol_binding: TRANSPORT_PROTOCOL_GRPC.to_string(),
681 protocol_version: crate::VERSION.to_string(),
682 tenant: Some("tenant-a".to_string()),
683 };
684
685 let json = serde_json::to_string(&iface).unwrap();
686 assert!(json.contains("\"url\":\"localhost:50051\""));
687
688 let back: AgentInterface = serde_json::from_str(&json).unwrap();
689 assert_eq!(back.url, "localhost:50051");
690 assert_eq!(back.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
691 assert_eq!(back.tenant.as_deref(), Some("tenant-a"));
692 }
693
694 #[cfg(feature = "fuzzing")]
705 #[test]
706 fn test_agent_interface_arbitrary_never_produces_unnormalized_grpc_url() {
707 use arbitrary::{Arbitrary, Unstructured};
708
709 #[rustfmt::skip]
710 let bytes: [u8; 14] = [
711 b'h', b't', b't', b'p', b':', b'/', b'/', b'x', b'g', b'r', b'p', b'c', 4, 8, ];
715 let mut u = Unstructured::new(&bytes);
716 let iface = AgentInterface::arbitrary(&mut u).unwrap();
717 assert!(
718 iface
719 .protocol_binding
720 .eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC)
721 );
722 assert_eq!(
723 iface.url, "x",
724 "arbitrary() must normalize the seed's http:// prefix away, same as new() would"
725 );
726
727 let json = serde_json::to_string(&iface).unwrap();
728 let back: AgentInterface = serde_json::from_str(&json).unwrap();
729 assert_eq!(iface, back, "did not round-trip: {json}");
730 }
731
732 #[test]
733 fn test_agent_card_deserialize_null_skills_as_default() {
734 let json = serde_json::json!({
735 "name": "Test Agent",
736 "description": "A test agent",
737 "version": "1.0.0",
738 "supportedInterfaces": [
739 {
740 "url": "http://localhost:3000",
741 "protocolBinding": "JSONRPC",
742 "protocolVersion": crate::VERSION
743 }
744 ],
745 "capabilities": {},
746 "defaultInputModes": ["text/plain"],
747 "defaultOutputModes": ["text/plain"],
748 "skills": null
749 });
750
751 let card: AgentCard = serde_json::from_value(json).unwrap();
752 assert!(card.skills.is_empty());
753 }
754
755 #[test]
759 fn test_agent_card_deserialize_defaults_optional_fields() {
760 let json = serde_json::json!({
761 "name": "Test Agent",
762 "version": "1.0.0",
763 "supportedInterfaces": [
764 {
765 "url": "http://localhost:3000",
766 "protocolBinding": "JSONRPC",
767 "protocolVersion": crate::VERSION
768 }
769 ],
770 "capabilities": {},
771 "skills": []
772 });
773
774 let card: AgentCard = serde_json::from_value(json).unwrap();
775 assert_eq!(card.description, "");
776 assert!(card.default_input_modes.is_empty());
777 assert!(card.default_output_modes.is_empty());
778 }
779
780 #[test]
781 fn test_security_scheme_deserialize_unknown_variant_errors() {
782 let err = serde_json::from_value::<SecurityScheme>(serde_json::json!({
783 "unknown": {"value": true}
784 }))
785 .unwrap_err();
786
787 assert!(err.to_string().contains("unknown security scheme variant"));
788 }
789
790 #[test]
791 fn test_oauth_flows_deserialize_unknown_variant_errors() {
792 let err = serde_json::from_value::<OAuthFlows>(serde_json::json!({
793 "unknown": {"tokenUrl": "https://example.com/token"}
794 }))
795 .unwrap_err();
796
797 assert!(err.to_string().contains("unknown OAuth flow variant"));
798 }
799
800 #[test]
801 fn test_security_scheme_apikey_serde() {
802 let ss = SecurityScheme::ApiKey(ApiKeySecurityScheme {
803 location: "header".to_string(),
804 name: "X-API-Key".to_string(),
805 description: None,
806 });
807 let json = serde_json::to_string(&ss).unwrap();
808 assert!(json.contains("apiKeySecurityScheme"));
809 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
810 assert_eq!(ss, back);
811 }
812
813 #[test]
814 fn test_security_scheme_httpauth_serde() {
815 let ss = SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
816 scheme: "Bearer".to_string(),
817 description: None,
818 bearer_format: Some("JWT".to_string()),
819 });
820 let json = serde_json::to_string(&ss).unwrap();
821 assert!(json.contains("httpAuthSecurityScheme"));
822 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
823 assert_eq!(ss, back);
824 }
825
826 #[test]
827 fn test_security_scheme_oauth2_serde() {
828 let ss = SecurityScheme::OAuth2(OAuth2SecurityScheme {
829 flows: OAuthFlows::ClientCredentials(ClientCredentialsOAuthFlow {
830 token_url: "https://auth.example.com/token".to_string(),
831 scopes: [("read".to_string(), "Read access".to_string())]
832 .into_iter()
833 .collect(),
834 refresh_url: None,
835 }),
836 description: None,
837 oauth2_metadata_url: None,
838 });
839 let json = serde_json::to_string(&ss).unwrap();
840 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
841 assert_eq!(ss, back);
842 }
843
844 #[test]
845 fn test_security_scheme_openidconnect_serde() {
846 let ss = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
847 open_id_connect_url: "https://example.com/.well-known/openid-configuration".to_string(),
848 description: None,
849 });
850 let json = serde_json::to_string(&ss).unwrap();
851 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
852 assert_eq!(ss, back);
853 }
854
855 #[test]
856 fn test_security_scheme_mtls_serde() {
857 let ss = SecurityScheme::MutualTls(MutualTlsSecurityScheme {
858 description: Some("mTLS auth".to_string()),
859 });
860 let json = serde_json::to_string(&ss).unwrap();
861 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
862 assert_eq!(ss, back);
863 }
864
865 #[test]
866 fn test_oauth_flows_all_variants() {
867 let flows = [
868 OAuthFlows::AuthorizationCode(AuthorizationCodeOAuthFlow {
869 authorization_url: "https://auth.example.com/authorize".to_string(),
870 token_url: "https://auth.example.com/token".to_string(),
871 scopes: HashMap::new(),
872 refresh_url: None,
873 pkce_required: Some(true),
874 }),
875 OAuthFlows::DeviceCode(DeviceCodeOAuthFlow {
876 device_authorization_url: "https://auth.example.com/device".to_string(),
877 token_url: "https://auth.example.com/token".to_string(),
878 scopes: HashMap::new(),
879 refresh_url: None,
880 }),
881 OAuthFlows::Implicit(ImplicitOAuthFlow {
882 authorization_url: "https://auth.example.com/authorize".to_string(),
883 scopes: HashMap::new(),
884 refresh_url: None,
885 }),
886 OAuthFlows::Password(PasswordOAuthFlow {
887 token_url: "https://auth.example.com/token".to_string(),
888 scopes: HashMap::new(),
889 refresh_url: None,
890 }),
891 ];
892 for flow in flows {
893 let json = serde_json::to_string(&flow).unwrap();
894 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
895 assert_eq!(flow, back);
896 }
897 }
898
899 #[test]
900 fn test_agent_capabilities_default() {
901 let caps = AgentCapabilities::default();
902 assert_eq!(caps.streaming, None);
903 assert_eq!(caps.push_notifications, None);
904 assert_eq!(caps.extensions, None);
905 assert_eq!(caps.extended_agent_card, None);
906 }
907
908 #[test]
909 fn test_agent_card_with_security_schemes() {
910 let mut schemes = HashMap::new();
911 schemes.insert(
912 "bearer".to_string(),
913 SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
914 scheme: "Bearer".to_string(),
915 description: None,
916 bearer_format: None,
917 }),
918 );
919 let card = AgentCard {
920 name: "Secure Agent".to_string(),
921 description: "Agent with auth".to_string(),
922 version: "1.0.0".to_string(),
923 supported_interfaces: vec![],
924 capabilities: AgentCapabilities::default(),
925 default_input_modes: vec![],
926 default_output_modes: vec![],
927 skills: vec![],
928 provider: None,
929 documentation_url: None,
930 icon_url: None,
931 security_schemes: Some(schemes),
932 security_requirements: Some(vec![
933 [("bearer".to_string(), vec![])].into_iter().collect(),
934 ]),
935 signatures: None,
936 };
937 let value = serde_json::to_value(&card).unwrap();
938 assert_eq!(
939 value["securityRequirements"],
940 serde_json::json!([{"schemes": {"bearer": {"list": []}}}])
941 );
942 let back: AgentCard = serde_json::from_value(value).unwrap();
943 assert_eq!(card, back);
944 }
945
946 #[test]
947 fn test_agent_skill_serializes_security_requirements() {
948 let skill = AgentSkill {
949 id: "secure-skill".to_string(),
950 name: "Secure skill".to_string(),
951 description: "Requires bearer authorization".to_string(),
952 tags: vec![],
953 examples: None,
954 input_modes: None,
955 output_modes: None,
956 security_requirements: Some(vec![
957 [("bearer".to_string(), vec!["read".to_string()])]
958 .into_iter()
959 .collect(),
960 ]),
961 };
962
963 let value = serde_json::to_value(&skill).unwrap();
964 assert_eq!(
965 value["securityRequirements"],
966 serde_json::json!([{"schemes": {"bearer": {"list": ["read"]}}}])
967 );
968 let back: AgentSkill = serde_json::from_value(value).unwrap();
969 assert_eq!(skill, back);
970 }
971
972 #[test]
973 fn test_agent_card_deserializes_null_skills_as_empty() {
974 let card: AgentCard = serde_json::from_str(
975 r#"{
976 "name": "Test Agent",
977 "description": "A test agent",
978 "version": "1.0.0",
979 "supportedInterfaces": [
980 {
981 "url": "http://localhost:3000",
982 "protocolBinding": "JSONRPC",
983 "protocolVersion": "1.0"
984 }
985 ],
986 "capabilities": {
987 "streaming": true
988 },
989 "defaultInputModes": ["text/plain"],
990 "defaultOutputModes": ["text/plain"],
991 "skills": null
992 }"#,
993 )
994 .unwrap();
995
996 assert!(card.skills.is_empty());
997 }
998
999 #[test]
1000 fn test_agent_card_deserializes_missing_skills_as_empty() {
1001 let card: AgentCard = serde_json::from_str(
1002 r#"{
1003 "name": "Test Agent",
1004 "description": "A test agent",
1005 "version": "1.0.0",
1006 "supportedInterfaces": [
1007 {
1008 "url": "http://localhost:3000",
1009 "protocolBinding": "JSONRPC",
1010 "protocolVersion": "1.0"
1011 }
1012 ],
1013 "capabilities": {
1014 "streaming": true
1015 },
1016 "defaultInputModes": ["text/plain"],
1017 "defaultOutputModes": ["text/plain"]
1018 }"#,
1019 )
1020 .unwrap();
1021
1022 assert!(card.skills.is_empty());
1023 }
1024
1025 #[test]
1026 fn test_agent_card_deserializes_wrapped_security_requirements() {
1027 let card: AgentCard = serde_json::from_str(
1028 r#"{
1029 "name": "Spec Agent",
1030 "description": "A test agent",
1031 "version": "1.0.0",
1032 "supportedInterfaces": [
1033 {
1034 "url": "https://example.com/spec",
1035 "protocolBinding": "JSONRPC",
1036 "protocolVersion": "1.0"
1037 }
1038 ],
1039 "capabilities": {
1040 "streaming": true
1041 },
1042 "defaultInputModes": ["text/plain"],
1043 "defaultOutputModes": ["text/plain"],
1044 "skills": [],
1045 "securityRequirements": [
1046 {
1047 "schemes": {
1048 "bearer_token": {
1049 "list": []
1050 }
1051 }
1052 }
1053 ]
1054 }"#,
1055 )
1056 .unwrap();
1057
1058 let requirements = card.security_requirements.unwrap();
1059 assert_eq!(requirements.len(), 1);
1060 assert_eq!(requirements[0].get("bearer_token"), Some(&Vec::new()));
1061 }
1062
1063 #[test]
1064 fn test_agent_card_deserializes_omitted_empty_security_scope_list() {
1065 let card: AgentCard = serde_json::from_str(
1066 r#"{
1067 "name": "Spec Agent",
1068 "description": "A test agent",
1069 "version": "1.0.0",
1070 "supportedInterfaces": [
1071 {
1072 "url": "https://example.com/spec",
1073 "protocolBinding": "JSONRPC",
1074 "protocolVersion": "1.0"
1075 }
1076 ],
1077 "capabilities": {
1078 "streaming": true
1079 },
1080 "defaultInputModes": ["text/plain"],
1081 "defaultOutputModes": ["text/plain"],
1082 "skills": [],
1083 "securityRequirements": [
1084 {
1085 "schemes": {
1086 "bearerAuth": {}
1087 }
1088 }
1089 ]
1090 }"#,
1091 )
1092 .unwrap();
1093
1094 let requirements = card.security_requirements.unwrap();
1095 assert_eq!(requirements.len(), 1);
1096 assert_eq!(requirements[0].get("bearerAuth"), Some(&Vec::new()));
1097 }
1098
1099 #[test]
1100 fn test_agent_card_rejects_security_scope_wrapper_without_list() {
1101 let err = serde_json::from_str::<AgentCard>(
1102 r#"{
1103 "name": "Spec Agent",
1104 "description": "A test agent",
1105 "version": "1.0.0",
1106 "supportedInterfaces": [
1107 {
1108 "url": "https://example.com/spec",
1109 "protocolBinding": "JSONRPC",
1110 "protocolVersion": "1.0"
1111 }
1112 ],
1113 "capabilities": {},
1114 "defaultInputModes": ["text/plain"],
1115 "defaultOutputModes": ["text/plain"],
1116 "skills": [],
1117 "securityRequirements": [
1118 {
1119 "schemes": {
1120 "bearerAuth": { "scopes": ["read"] }
1121 }
1122 }
1123 ]
1124 }"#,
1125 )
1126 .unwrap_err();
1127
1128 assert!(
1129 err.to_string()
1130 .contains("invalid wrapped security scopes for bearerAuth"),
1131 "{err}"
1132 );
1133 }
1134}