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        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// ---------------------------------------------------------------------------
136// AgentInterface
137// ---------------------------------------------------------------------------
138
139/// A URL + protocol binding combination for reaching the agent.
140#[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// A derived `Arbitrary` would fill `url` independently of `protocol_binding`,
149// producing combinations `new()` never does (e.g. a `grpc` binding paired
150// with an un-normalized `http://` URL) -- valid per the type but not
151// round-trip-stable, since `Serialize`/`Deserialize` always normalize.
152// Route through the same normalization instead, so a fuzz-generated value
153// is exactly as a real caller's `new()` would have built it.
154#[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// ---------------------------------------------------------------------------
234// AgentProvider
235// ---------------------------------------------------------------------------
236
237/// Information about the agent's service provider.
238#[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// ---------------------------------------------------------------------------
247// AgentCapabilities
248// ---------------------------------------------------------------------------
249
250/// Optional capabilities supported by an agent.
251#[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// ---------------------------------------------------------------------------
269// AgentExtension
270// ---------------------------------------------------------------------------
271
272/// A protocol extension supported by the agent.
273#[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// ---------------------------------------------------------------------------
291// AgentSkill
292// ---------------------------------------------------------------------------
293
294/// A distinct capability or function that an agent can perform.
295#[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// ---------------------------------------------------------------------------
322// SecurityScheme (field-presence union)
323// ---------------------------------------------------------------------------
324
325/// A security scheme for authorizing requests, following OpenAPI 3.0.
326#[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// ---------------------------------------------------------------------------
383// Security scheme types
384// ---------------------------------------------------------------------------
385
386#[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// ---------------------------------------------------------------------------
436// OAuth flows (field-presence union)
437// ---------------------------------------------------------------------------
438
439#[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
547// ---------------------------------------------------------------------------
548// Security requirement
549// ---------------------------------------------------------------------------
550
551/// A security requirement: map of scheme name → required scopes.
552pub type SecurityRequirement = HashMap<String, Vec<String>>;
553
554// ---------------------------------------------------------------------------
555// AgentCardSignature
556// ---------------------------------------------------------------------------
557
558/// JWS signature for an AgentCard (RFC 7515).
559#[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    // `agent_card_roundtrip`'s fuzz oracle assumes any arbitrary-generated
656    // AgentCard is serde-round-trip-identical; a derived Arbitrary broke that
657    // for exactly the state above (grpc binding + un-normalized http:// url),
658    // since it fills fields independently of `new()`. Hitting that exact
659    // combination by chance is astronomically unlikely (protocol_binding has
660    // to land on the literal string "grpc"), so this drives `arbitrary`
661    // directly with a byte sequence hand-derived from arbitrary 1.4's own
662    // length-then-content encoding (see arbitrary_byte_size/arbitrary_str in
663    // its `Unstructured`) to force exactly that combination, rather than
664    // relying on a fixed seed or many random ones to stumble onto it.
665    #[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', // url content: "http://x"
673            b'g', b'r', b'p', b'c',                          // protocol_binding: "grpc"
674            4, 8, // trailing length bytes, consumed back-to-front: proto=4, url=8
675        ];
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    /// #275 (SHOULD-level): `description`, `defaultInputModes` and
717    /// `defaultOutputModes` should default when absent rather than failing
718    /// deserialization outright.
719    #[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}