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