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)]
141pub struct AgentInterface {
142 pub url: String,
143 pub protocol_binding: TransportProtocol,
144 pub protocol_version: ProtocolVersion,
145 pub tenant: Option<String>,
146}
147
148#[cfg(feature = "fuzzing")]
155impl<'a> arbitrary::Arbitrary<'a> for AgentInterface {
156 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
157 let url = String::arbitrary(u)?;
158 let protocol_binding = TransportProtocol::arbitrary(u)?;
159 Ok(AgentInterface {
160 url: normalize_agent_interface_url(url, &protocol_binding),
161 protocol_binding,
162 protocol_version: ProtocolVersion::arbitrary(u)?,
163 tenant: Option::<String>::arbitrary(u)?,
164 })
165 }
166}
167
168#[derive(Deserialize)]
169#[serde(rename_all = "camelCase")]
170struct AgentInterfaceSerde {
171 url: String,
172 protocol_binding: TransportProtocol,
173 protocol_version: ProtocolVersion,
174 #[serde(default)]
175 tenant: Option<String>,
176}
177
178fn normalize_agent_interface_url(url: String, protocol_binding: &str) -> String {
179 if protocol_binding.eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC) {
180 if let Some(stripped) = url.strip_prefix("http://") {
181 return stripped.to_string();
182 }
183 }
184
185 url
186}
187
188impl AgentInterface {
189 pub fn new(url: impl Into<String>, protocol_binding: impl Into<String>) -> Self {
190 let protocol_binding = protocol_binding.into();
191 AgentInterface {
192 url: normalize_agent_interface_url(url.into(), &protocol_binding),
193 protocol_binding,
194 protocol_version: crate::VERSION.to_string(),
195 tenant: None,
196 }
197 }
198
199 pub fn wire_url(&self) -> String {
200 normalize_agent_interface_url(self.url.clone(), &self.protocol_binding)
201 }
202}
203
204impl Serialize for AgentInterface {
205 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
206 use serde::ser::SerializeStruct;
207
208 let mut state = serializer
209 .serialize_struct("AgentInterface", if self.tenant.is_some() { 4 } else { 3 })?;
210 state.serialize_field("url", &self.wire_url())?;
211 state.serialize_field("protocolBinding", &self.protocol_binding)?;
212 state.serialize_field("protocolVersion", &self.protocol_version)?;
213 if let Some(tenant) = &self.tenant {
214 state.serialize_field("tenant", tenant)?;
215 }
216 state.end()
217 }
218}
219
220impl<'de> Deserialize<'de> for AgentInterface {
221 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222 let raw = AgentInterfaceSerde::deserialize(deserializer)?;
223
224 Ok(Self {
225 url: normalize_agent_interface_url(raw.url, &raw.protocol_binding),
226 protocol_binding: raw.protocol_binding,
227 protocol_version: raw.protocol_version,
228 tenant: raw.tenant,
229 })
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
240#[serde(rename_all = "camelCase")]
241pub struct AgentProvider {
242 pub organization: String,
243 pub url: String,
244}
245
246#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
252#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
253#[serde(rename_all = "camelCase")]
254pub struct AgentCapabilities {
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub streaming: Option<bool>,
257
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub push_notifications: Option<bool>,
260
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub extensions: Option<Vec<AgentExtension>>,
263
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub extended_agent_card: Option<bool>,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
275#[serde(rename_all = "camelCase")]
276pub struct AgentExtension {
277 pub uri: String,
278
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub description: Option<String>,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub required: Option<bool>,
284
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 #[cfg_attr(feature = "fuzzing", arbitrary(default))]
287 pub params: Option<HashMap<String, Value>>,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
297#[serde(rename_all = "camelCase")]
298pub struct AgentSkill {
299 pub id: String,
300 pub name: String,
301 pub description: String,
302 pub tags: Vec<String>,
303
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub examples: Option<Vec<String>>,
306
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub input_modes: Option<Vec<String>>,
309
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub output_modes: Option<Vec<String>>,
312
313 #[serde(
314 default,
315 skip_serializing_if = "Option::is_none",
316 deserialize_with = "deserialize_optional_security_requirements"
317 )]
318 pub security_requirements: Option<Vec<SecurityRequirement>>,
319}
320
321#[derive(Debug, Clone, PartialEq)]
327#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
328pub enum SecurityScheme {
329 ApiKey(ApiKeySecurityScheme),
330 HttpAuth(HttpAuthSecurityScheme),
331 OAuth2(OAuth2SecurityScheme),
332 OpenIdConnect(OpenIdConnectSecurityScheme),
333 MutualTls(MutualTlsSecurityScheme),
334}
335
336impl Serialize for SecurityScheme {
337 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
338 use serde::ser::SerializeMap;
339 let mut map = serializer.serialize_map(Some(1))?;
340 match self {
341 SecurityScheme::ApiKey(s) => map.serialize_entry("apiKeySecurityScheme", s)?,
342 SecurityScheme::HttpAuth(s) => map.serialize_entry("httpAuthSecurityScheme", s)?,
343 SecurityScheme::OAuth2(s) => map.serialize_entry("oauth2SecurityScheme", s)?,
344 SecurityScheme::OpenIdConnect(s) => {
345 map.serialize_entry("openIdConnectSecurityScheme", s)?
346 }
347 SecurityScheme::MutualTls(s) => map.serialize_entry("mtlsSecurityScheme", s)?,
348 }
349 map.end()
350 }
351}
352
353impl<'de> Deserialize<'de> for SecurityScheme {
354 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
355 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
356 if let Some(v) = raw.get("apiKeySecurityScheme") {
357 Ok(SecurityScheme::ApiKey(
358 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
359 ))
360 } else if let Some(v) = raw.get("httpAuthSecurityScheme") {
361 Ok(SecurityScheme::HttpAuth(
362 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
363 ))
364 } else if let Some(v) = raw.get("oauth2SecurityScheme") {
365 Ok(SecurityScheme::OAuth2(
366 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
367 ))
368 } else if let Some(v) = raw.get("openIdConnectSecurityScheme") {
369 Ok(SecurityScheme::OpenIdConnect(
370 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
371 ))
372 } else if let Some(v) = raw.get("mtlsSecurityScheme") {
373 Ok(SecurityScheme::MutualTls(
374 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
375 ))
376 } else {
377 Err(serde::de::Error::custom("unknown security scheme variant"))
378 }
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
387#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
388#[serde(rename_all = "camelCase")]
389pub struct ApiKeySecurityScheme {
390 pub location: String,
391 pub name: String,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub description: Option<String>,
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
398#[serde(rename_all = "camelCase")]
399pub struct HttpAuthSecurityScheme {
400 pub scheme: String,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub description: Option<String>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub bearer_format: Option<String>,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
409#[serde(rename_all = "camelCase")]
410pub struct OAuth2SecurityScheme {
411 pub flows: OAuthFlows,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub description: Option<String>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub oauth2_metadata_url: Option<String>,
416}
417
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
420#[serde(rename_all = "camelCase")]
421pub struct OpenIdConnectSecurityScheme {
422 pub open_id_connect_url: String,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub description: Option<String>,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
428#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
429#[serde(rename_all = "camelCase")]
430pub struct MutualTlsSecurityScheme {
431 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub description: Option<String>,
433}
434
435#[derive(Debug, Clone, PartialEq)]
440#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
441pub enum OAuthFlows {
442 AuthorizationCode(AuthorizationCodeOAuthFlow),
443 ClientCredentials(ClientCredentialsOAuthFlow),
444 DeviceCode(DeviceCodeOAuthFlow),
445 Implicit(ImplicitOAuthFlow),
446 Password(PasswordOAuthFlow),
447}
448
449impl Serialize for OAuthFlows {
450 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
451 use serde::ser::SerializeMap;
452 let mut map = serializer.serialize_map(Some(1))?;
453 match self {
454 OAuthFlows::AuthorizationCode(f) => map.serialize_entry("authorizationCode", f)?,
455 OAuthFlows::ClientCredentials(f) => map.serialize_entry("clientCredentials", f)?,
456 OAuthFlows::DeviceCode(f) => map.serialize_entry("deviceCode", f)?,
457 OAuthFlows::Implicit(f) => map.serialize_entry("implicit", f)?,
458 OAuthFlows::Password(f) => map.serialize_entry("password", f)?,
459 }
460 map.end()
461 }
462}
463
464impl<'de> Deserialize<'de> for OAuthFlows {
465 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
466 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
467 if let Some(v) = raw.get("authorizationCode") {
468 Ok(OAuthFlows::AuthorizationCode(
469 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
470 ))
471 } else if let Some(v) = raw.get("clientCredentials") {
472 Ok(OAuthFlows::ClientCredentials(
473 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
474 ))
475 } else if let Some(v) = raw.get("deviceCode") {
476 Ok(OAuthFlows::DeviceCode(
477 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
478 ))
479 } else if let Some(v) = raw.get("implicit") {
480 Ok(OAuthFlows::Implicit(
481 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
482 ))
483 } else if let Some(v) = raw.get("password") {
484 Ok(OAuthFlows::Password(
485 serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?,
486 ))
487 } else {
488 Err(serde::de::Error::custom("unknown OAuth flow variant"))
489 }
490 }
491}
492
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
495#[serde(rename_all = "camelCase")]
496pub struct AuthorizationCodeOAuthFlow {
497 pub authorization_url: String,
498 pub token_url: String,
499 pub scopes: HashMap<String, String>,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub refresh_url: Option<String>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub pkce_required: Option<bool>,
504}
505
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
508#[serde(rename_all = "camelCase")]
509pub struct ClientCredentialsOAuthFlow {
510 pub token_url: String,
511 pub scopes: HashMap<String, String>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub refresh_url: Option<String>,
514}
515
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
518#[serde(rename_all = "camelCase")]
519pub struct DeviceCodeOAuthFlow {
520 pub device_authorization_url: String,
521 pub token_url: String,
522 pub scopes: HashMap<String, String>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub refresh_url: Option<String>,
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
529#[serde(rename_all = "camelCase")]
530pub struct ImplicitOAuthFlow {
531 pub authorization_url: String,
532 pub scopes: HashMap<String, String>,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub refresh_url: Option<String>,
535}
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
539#[serde(rename_all = "camelCase")]
540pub struct PasswordOAuthFlow {
541 pub token_url: String,
542 pub scopes: HashMap<String, String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub refresh_url: Option<String>,
545}
546
547pub type SecurityRequirement = HashMap<String, Vec<String>>;
553
554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
560#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
561#[serde(rename_all = "camelCase")]
562pub struct AgentCardSignature {
563 pub protected: String,
564 pub signature: String,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 #[cfg_attr(feature = "fuzzing", arbitrary(default))]
567 pub header: Option<HashMap<String, Value>>,
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn test_agent_card_serde() {
576 let card = AgentCard {
577 name: "Test Agent".to_string(),
578 description: "A test agent".to_string(),
579 version: "1.0.0".to_string(),
580 supported_interfaces: vec![AgentInterface::new("http://localhost:3000", "JSONRPC")],
581 capabilities: AgentCapabilities {
582 streaming: Some(true),
583 push_notifications: Some(false),
584 extensions: None,
585 extended_agent_card: None,
586 },
587 default_input_modes: vec!["text/plain".to_string()],
588 default_output_modes: vec!["text/plain".to_string()],
589 skills: vec![AgentSkill {
590 id: "echo".to_string(),
591 name: "Echo".to_string(),
592 description: "Echoes input".to_string(),
593 tags: vec!["test".to_string()],
594 examples: Some(vec!["hello".to_string()]),
595 input_modes: None,
596 output_modes: None,
597 security_requirements: None,
598 }],
599 provider: Some(AgentProvider {
600 organization: "Test Corp".to_string(),
601 url: "https://test.com".to_string(),
602 }),
603 documentation_url: None,
604 icon_url: None,
605 security_schemes: None,
606 security_requirements: None,
607 signatures: None,
608 };
609
610 let json = serde_json::to_string(&card).unwrap();
611 let back: AgentCard = serde_json::from_str(&json).unwrap();
612 assert_eq!(card, back);
613 }
614
615 #[test]
616 fn test_agent_interface_new() {
617 let iface = AgentInterface::new("http://localhost:3000", "JSONRPC");
618 assert_eq!(iface.url, "http://localhost:3000");
619 assert_eq!(iface.protocol_binding, "JSONRPC");
620 assert!(!iface.protocol_version.is_empty());
621 }
622
623 #[test]
624 fn test_agent_interface_new_normalizes_grpc_http_scheme() {
625 let iface = AgentInterface::new("http://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
626 assert_eq!(iface.url, "localhost:50051");
627 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
628 }
629
630 #[test]
631 fn test_agent_interface_new_preserves_grpc_https_scheme() {
632 let iface = AgentInterface::new("https://localhost:50051", TRANSPORT_PROTOCOL_GRPC);
633 assert_eq!(iface.url, "https://localhost:50051");
634 assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
635 }
636
637 #[test]
638 fn test_agent_interface_serde_normalizes_grpc_http_scheme() {
639 let iface = AgentInterface {
640 url: "http://localhost:50051".to_string(),
641 protocol_binding: TRANSPORT_PROTOCOL_GRPC.to_string(),
642 protocol_version: crate::VERSION.to_string(),
643 tenant: Some("tenant-a".to_string()),
644 };
645
646 let json = serde_json::to_string(&iface).unwrap();
647 assert!(json.contains("\"url\":\"localhost:50051\""));
648
649 let back: AgentInterface = serde_json::from_str(&json).unwrap();
650 assert_eq!(back.url, "localhost:50051");
651 assert_eq!(back.protocol_binding, TRANSPORT_PROTOCOL_GRPC);
652 assert_eq!(back.tenant.as_deref(), Some("tenant-a"));
653 }
654
655 #[cfg(feature = "fuzzing")]
666 #[test]
667 fn test_agent_interface_arbitrary_never_produces_unnormalized_grpc_url() {
668 use arbitrary::{Arbitrary, Unstructured};
669
670 #[rustfmt::skip]
671 let bytes: [u8; 14] = [
672 b'h', b't', b't', b'p', b':', b'/', b'/', b'x', b'g', b'r', b'p', b'c', 4, 8, ];
676 let mut u = Unstructured::new(&bytes);
677 let iface = AgentInterface::arbitrary(&mut u).unwrap();
678 assert!(
679 iface
680 .protocol_binding
681 .eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC)
682 );
683 assert_eq!(
684 iface.url, "x",
685 "arbitrary() must normalize the seed's http:// prefix away, same as new() would"
686 );
687
688 let json = serde_json::to_string(&iface).unwrap();
689 let back: AgentInterface = serde_json::from_str(&json).unwrap();
690 assert_eq!(iface, back, "did not round-trip: {json}");
691 }
692
693 #[test]
694 fn test_agent_card_deserialize_null_skills_as_default() {
695 let json = serde_json::json!({
696 "name": "Test Agent",
697 "description": "A test agent",
698 "version": "1.0.0",
699 "supportedInterfaces": [
700 {
701 "url": "http://localhost:3000",
702 "protocolBinding": "JSONRPC",
703 "protocolVersion": crate::VERSION
704 }
705 ],
706 "capabilities": {},
707 "defaultInputModes": ["text/plain"],
708 "defaultOutputModes": ["text/plain"],
709 "skills": null
710 });
711
712 let card: AgentCard = serde_json::from_value(json).unwrap();
713 assert!(card.skills.is_empty());
714 }
715
716 #[test]
720 fn test_agent_card_deserialize_defaults_optional_fields() {
721 let json = serde_json::json!({
722 "name": "Test Agent",
723 "version": "1.0.0",
724 "supportedInterfaces": [
725 {
726 "url": "http://localhost:3000",
727 "protocolBinding": "JSONRPC",
728 "protocolVersion": crate::VERSION
729 }
730 ],
731 "capabilities": {},
732 "skills": []
733 });
734
735 let card: AgentCard = serde_json::from_value(json).unwrap();
736 assert_eq!(card.description, "");
737 assert!(card.default_input_modes.is_empty());
738 assert!(card.default_output_modes.is_empty());
739 }
740
741 #[test]
742 fn test_security_scheme_deserialize_unknown_variant_errors() {
743 let err = serde_json::from_value::<SecurityScheme>(serde_json::json!({
744 "unknown": {"value": true}
745 }))
746 .unwrap_err();
747
748 assert!(err.to_string().contains("unknown security scheme variant"));
749 }
750
751 #[test]
752 fn test_oauth_flows_deserialize_unknown_variant_errors() {
753 let err = serde_json::from_value::<OAuthFlows>(serde_json::json!({
754 "unknown": {"tokenUrl": "https://example.com/token"}
755 }))
756 .unwrap_err();
757
758 assert!(err.to_string().contains("unknown OAuth flow variant"));
759 }
760
761 #[test]
762 fn test_security_scheme_apikey_serde() {
763 let ss = SecurityScheme::ApiKey(ApiKeySecurityScheme {
764 location: "header".to_string(),
765 name: "X-API-Key".to_string(),
766 description: None,
767 });
768 let json = serde_json::to_string(&ss).unwrap();
769 assert!(json.contains("apiKeySecurityScheme"));
770 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
771 assert_eq!(ss, back);
772 }
773
774 #[test]
775 fn test_security_scheme_httpauth_serde() {
776 let ss = SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
777 scheme: "Bearer".to_string(),
778 description: None,
779 bearer_format: Some("JWT".to_string()),
780 });
781 let json = serde_json::to_string(&ss).unwrap();
782 assert!(json.contains("httpAuthSecurityScheme"));
783 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
784 assert_eq!(ss, back);
785 }
786
787 #[test]
788 fn test_security_scheme_oauth2_serde() {
789 let ss = SecurityScheme::OAuth2(OAuth2SecurityScheme {
790 flows: OAuthFlows::ClientCredentials(ClientCredentialsOAuthFlow {
791 token_url: "https://auth.example.com/token".to_string(),
792 scopes: [("read".to_string(), "Read access".to_string())]
793 .into_iter()
794 .collect(),
795 refresh_url: None,
796 }),
797 description: None,
798 oauth2_metadata_url: None,
799 });
800 let json = serde_json::to_string(&ss).unwrap();
801 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
802 assert_eq!(ss, back);
803 }
804
805 #[test]
806 fn test_security_scheme_openidconnect_serde() {
807 let ss = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
808 open_id_connect_url: "https://example.com/.well-known/openid-configuration".to_string(),
809 description: None,
810 });
811 let json = serde_json::to_string(&ss).unwrap();
812 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
813 assert_eq!(ss, back);
814 }
815
816 #[test]
817 fn test_security_scheme_mtls_serde() {
818 let ss = SecurityScheme::MutualTls(MutualTlsSecurityScheme {
819 description: Some("mTLS auth".to_string()),
820 });
821 let json = serde_json::to_string(&ss).unwrap();
822 let back: SecurityScheme = serde_json::from_str(&json).unwrap();
823 assert_eq!(ss, back);
824 }
825
826 #[test]
827 fn test_oauth_flows_all_variants() {
828 let flows = [
829 OAuthFlows::AuthorizationCode(AuthorizationCodeOAuthFlow {
830 authorization_url: "https://auth.example.com/authorize".to_string(),
831 token_url: "https://auth.example.com/token".to_string(),
832 scopes: HashMap::new(),
833 refresh_url: None,
834 pkce_required: Some(true),
835 }),
836 OAuthFlows::DeviceCode(DeviceCodeOAuthFlow {
837 device_authorization_url: "https://auth.example.com/device".to_string(),
838 token_url: "https://auth.example.com/token".to_string(),
839 scopes: HashMap::new(),
840 refresh_url: None,
841 }),
842 OAuthFlows::Implicit(ImplicitOAuthFlow {
843 authorization_url: "https://auth.example.com/authorize".to_string(),
844 scopes: HashMap::new(),
845 refresh_url: None,
846 }),
847 OAuthFlows::Password(PasswordOAuthFlow {
848 token_url: "https://auth.example.com/token".to_string(),
849 scopes: HashMap::new(),
850 refresh_url: None,
851 }),
852 ];
853 for flow in flows {
854 let json = serde_json::to_string(&flow).unwrap();
855 let back: OAuthFlows = serde_json::from_str(&json).unwrap();
856 assert_eq!(flow, back);
857 }
858 }
859
860 #[test]
861 fn test_agent_capabilities_default() {
862 let caps = AgentCapabilities::default();
863 assert_eq!(caps.streaming, None);
864 assert_eq!(caps.push_notifications, None);
865 assert_eq!(caps.extensions, None);
866 assert_eq!(caps.extended_agent_card, None);
867 }
868
869 #[test]
870 fn test_agent_card_with_security_schemes() {
871 let mut schemes = HashMap::new();
872 schemes.insert(
873 "bearer".to_string(),
874 SecurityScheme::HttpAuth(HttpAuthSecurityScheme {
875 scheme: "Bearer".to_string(),
876 description: None,
877 bearer_format: None,
878 }),
879 );
880 let card = AgentCard {
881 name: "Secure Agent".to_string(),
882 description: "Agent with auth".to_string(),
883 version: "1.0.0".to_string(),
884 supported_interfaces: vec![],
885 capabilities: AgentCapabilities::default(),
886 default_input_modes: vec![],
887 default_output_modes: vec![],
888 skills: vec![],
889 provider: None,
890 documentation_url: None,
891 icon_url: None,
892 security_schemes: Some(schemes),
893 security_requirements: Some(vec![
894 [("bearer".to_string(), vec![])].into_iter().collect(),
895 ]),
896 signatures: None,
897 };
898 let json = serde_json::to_string(&card).unwrap();
899 let back: AgentCard = serde_json::from_str(&json).unwrap();
900 assert_eq!(card, back);
901 }
902
903 #[test]
904 fn test_agent_card_deserializes_null_skills_as_empty() {
905 let card: AgentCard = serde_json::from_str(
906 r#"{
907 "name": "Test Agent",
908 "description": "A test agent",
909 "version": "1.0.0",
910 "supportedInterfaces": [
911 {
912 "url": "http://localhost:3000",
913 "protocolBinding": "JSONRPC",
914 "protocolVersion": "1.0"
915 }
916 ],
917 "capabilities": {
918 "streaming": true
919 },
920 "defaultInputModes": ["text/plain"],
921 "defaultOutputModes": ["text/plain"],
922 "skills": null
923 }"#,
924 )
925 .unwrap();
926
927 assert!(card.skills.is_empty());
928 }
929
930 #[test]
931 fn test_agent_card_deserializes_missing_skills_as_empty() {
932 let card: AgentCard = serde_json::from_str(
933 r#"{
934 "name": "Test Agent",
935 "description": "A test agent",
936 "version": "1.0.0",
937 "supportedInterfaces": [
938 {
939 "url": "http://localhost:3000",
940 "protocolBinding": "JSONRPC",
941 "protocolVersion": "1.0"
942 }
943 ],
944 "capabilities": {
945 "streaming": true
946 },
947 "defaultInputModes": ["text/plain"],
948 "defaultOutputModes": ["text/plain"]
949 }"#,
950 )
951 .unwrap();
952
953 assert!(card.skills.is_empty());
954 }
955
956 #[test]
957 fn test_agent_card_deserializes_wrapped_security_requirements() {
958 let card: AgentCard = serde_json::from_str(
959 r#"{
960 "name": "Spec Agent",
961 "description": "A test agent",
962 "version": "1.0.0",
963 "supportedInterfaces": [
964 {
965 "url": "https://example.com/spec",
966 "protocolBinding": "JSONRPC",
967 "protocolVersion": "1.0"
968 }
969 ],
970 "capabilities": {
971 "streaming": true
972 },
973 "defaultInputModes": ["text/plain"],
974 "defaultOutputModes": ["text/plain"],
975 "skills": [],
976 "securityRequirements": [
977 {
978 "schemes": {
979 "bearer_token": {
980 "list": []
981 }
982 }
983 }
984 ]
985 }"#,
986 )
987 .unwrap();
988
989 let requirements = card.security_requirements.unwrap();
990 assert_eq!(requirements.len(), 1);
991 assert_eq!(requirements[0].get("bearer_token"), Some(&Vec::new()));
992 }
993}