Skip to main content

a2a/
agent_card.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// Copyright A2A Contributors (https://github.com/a2aproject)
3// SPDX-License-Identifier: Apache-2.0
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6use std::collections::HashMap;
7
8use crate::types::{ProtocolVersion, TRANSPORT_PROTOCOL_GRPC, TransportProtocol};
9
10// ---------------------------------------------------------------------------
11// AgentCard
12// ---------------------------------------------------------------------------
13
14/// Self-describing manifest for an agent.
15#[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                // ProtoJSON omits an empty StringList, so {} is the same as {"list": []}.
153                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// ---------------------------------------------------------------------------
174// AgentInterface
175// ---------------------------------------------------------------------------
176
177/// A URL + protocol binding combination for reaching the agent.
178#[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// A derived `Arbitrary` would fill `url` independently of `protocol_binding`,
187// producing combinations `new()` never does (e.g. a `grpc` binding paired
188// with an un-normalized `http://` URL) -- valid per the type but not
189// round-trip-stable, since `Serialize`/`Deserialize` always normalize.
190// Route through the same normalization instead, so a fuzz-generated value
191// is exactly as a real caller's `new()` would have built it.
192#[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// ---------------------------------------------------------------------------
272// AgentProvider
273// ---------------------------------------------------------------------------
274
275/// Information about the agent's service provider.
276#[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// ---------------------------------------------------------------------------
285// AgentCapabilities
286// ---------------------------------------------------------------------------
287
288/// Optional capabilities supported by an agent.
289#[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// ---------------------------------------------------------------------------
307// AgentExtension
308// ---------------------------------------------------------------------------
309
310/// A protocol extension supported by the agent.
311#[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// ---------------------------------------------------------------------------
329// AgentSkill
330// ---------------------------------------------------------------------------
331
332/// A distinct capability or function that an agent can perform.
333#[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// ---------------------------------------------------------------------------
361// SecurityScheme (field-presence union)
362// ---------------------------------------------------------------------------
363
364/// A security scheme for authorizing requests, following OpenAPI 3.0.
365#[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// ---------------------------------------------------------------------------
422// Security scheme types
423// ---------------------------------------------------------------------------
424
425#[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// ---------------------------------------------------------------------------
475// OAuth flows (field-presence union)
476// ---------------------------------------------------------------------------
477
478#[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
586// ---------------------------------------------------------------------------
587// Security requirement
588// ---------------------------------------------------------------------------
589
590/// A security requirement: map of scheme name → required scopes.
591pub type SecurityRequirement = HashMap<String, Vec<String>>;
592
593// ---------------------------------------------------------------------------
594// AgentCardSignature
595// ---------------------------------------------------------------------------
596
597/// JWS signature for an AgentCard (RFC 7515).
598#[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    // `agent_card_roundtrip`'s fuzz oracle assumes any arbitrary-generated
695    // AgentCard is serde-round-trip-identical; a derived Arbitrary broke that
696    // for exactly the state above (grpc binding + un-normalized http:// url),
697    // since it fills fields independently of `new()`. Hitting that exact
698    // combination by chance is astronomically unlikely (protocol_binding has
699    // to land on the literal string "grpc"), so this drives `arbitrary`
700    // directly with a byte sequence hand-derived from arbitrary 1.4's own
701    // length-then-content encoding (see arbitrary_byte_size/arbitrary_str in
702    // its `Unstructured`) to force exactly that combination, rather than
703    // relying on a fixed seed or many random ones to stumble onto it.
704    #[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', // url content: "http://x"
712            b'g', b'r', b'p', b'c',                          // protocol_binding: "grpc"
713            4, 8, // trailing length bytes, consumed back-to-front: proto=4, url=8
714        ];
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    /// #275 (SHOULD-level): `description`, `defaultInputModes` and
756    /// `defaultOutputModes` should default when absent rather than failing
757    /// deserialization outright.
758    #[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}