Skip to main content

a2a/
agent_card.rs

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