Skip to main content

a2a_rust/types/
agent_card.rs

1use std::collections::BTreeMap;
2
3use base64::Engine as _;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8use crate::types::JsonObject;
9
10use super::security::{SecurityRequirement, SecurityScheme};
11
12/// Agent discovery document served from `/.well-known/agent-card.json`.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct AgentCard {
16    /// Human-readable agent name.
17    pub name: String,
18    /// Human-readable agent description.
19    pub description: String,
20    /// Ordered list of supported transport bindings.
21    pub supported_interfaces: Vec<AgentInterface>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    /// Optional agent provider metadata.
24    pub provider: Option<AgentProvider>,
25    /// Agent implementation version string.
26    pub version: String,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    /// Optional human-readable documentation URL.
29    pub documentation_url: Option<String>,
30    /// Capability flags and advertised extensions.
31    pub capabilities: AgentCapabilities,
32    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33    /// Named security schemes referenced by requirements.
34    pub security_schemes: BTreeMap<String, SecurityScheme>,
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    /// Security requirements that apply by default.
37    pub security_requirements: Vec<SecurityRequirement>,
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    /// Default accepted input modes.
40    pub default_input_modes: Vec<String>,
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    /// Default produced output modes.
43    pub default_output_modes: Vec<String>,
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    /// Skills exposed by the agent.
46    pub skills: Vec<AgentSkill>,
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    /// Optional signatures over the agent card.
49    pub signatures: Vec<AgentCardSignature>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    /// Optional icon URL for UI presentation.
52    pub icon_url: Option<String>,
53}
54
55/// Transport binding advertised by an agent card.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct AgentInterface {
59    /// Absolute or relative interface URL.
60    pub url: String,
61    /// Binding name such as `JSONRPC` or `HTTP+JSON`.
62    pub protocol_binding: String,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    /// Optional tenant associated with the interface.
65    pub tenant: Option<String>,
66    /// Protocol version served from the interface.
67    pub protocol_version: String,
68}
69
70/// Organization metadata for the agent provider.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct AgentProvider {
74    /// Provider homepage URL.
75    pub url: String,
76    /// Provider or organization name.
77    pub organization: String,
78}
79
80/// Capability flags and extension declarations for an agent.
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct AgentCapabilities {
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    /// Whether streaming operations are supported.
86    pub streaming: Option<bool>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    /// Whether push-notification configuration APIs are supported.
89    pub push_notifications: Option<bool>,
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    /// Protocol extensions advertised by the agent.
92    pub extensions: Vec<AgentExtension>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    /// Whether `GetExtendedAgentCard` is supported.
95    pub extended_agent_card: Option<bool>,
96}
97
98/// Extension declaration inside `AgentCapabilities`.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct AgentExtension {
102    /// Stable extension URI.
103    pub uri: String,
104    #[serde(default, skip_serializing_if = "String::is_empty")]
105    /// Human-readable extension description.
106    pub description: String,
107    #[serde(default, skip_serializing_if = "crate::types::is_false")]
108    /// Whether the extension is required to interoperate.
109    pub required: bool,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    /// Optional extension-specific parameters.
112    pub params: Option<JsonObject>,
113}
114
115/// Skill advertised by an agent card.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct AgentSkill {
119    /// Stable skill identifier.
120    pub id: String,
121    /// Human-readable skill name.
122    pub name: String,
123    /// Human-readable skill description.
124    pub description: String,
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    /// Searchable skill tags.
127    pub tags: Vec<String>,
128    #[serde(default, skip_serializing_if = "Vec::is_empty")]
129    /// Example prompts or invocations for the skill.
130    pub examples: Vec<String>,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    /// Input modes supported by the skill.
133    pub input_modes: Vec<String>,
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    /// Output modes produced by the skill.
136    pub output_modes: Vec<String>,
137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138    /// Security requirements specific to the skill.
139    pub security_requirements: Vec<SecurityRequirement>,
140}
141
142/// Signature over an agent card payload.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct AgentCardSignature {
146    /// Protected JOSE header segment.
147    pub protected: String,
148    /// Signature bytes encoded as a string.
149    pub signature: String,
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    /// Optional unprotected JOSE header values.
152    pub header: Option<JsonObject>,
153}
154
155/// Decoded protected header for an agent-card detached JWS signature.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct JwsProtectedHeader {
159    /// JWS algorithm identifier such as `ES256`.
160    pub alg: String,
161    /// Key identifier used to resolve the verification key.
162    pub kid: String,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    /// Optional JOSE type, typically `JOSE`.
165    pub typ: Option<String>,
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    /// Optional JWK Set URL that can help callers resolve keys.
168    pub jku: Option<String>,
169    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
170    /// Additional JOSE header parameters.
171    pub extra: BTreeMap<String, Value>,
172}
173
174/// Prepared detached-JWS verification input for an agent-card signature.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct AgentCardSignatureVerificationInput {
177    /// Parsed protected JOSE header.
178    pub protected_header: JwsProtectedHeader,
179    /// Original base64url-encoded protected segment.
180    pub protected_segment: String,
181    /// Decoded signature bytes.
182    pub signature: Vec<u8>,
183    /// Detached JWS signing input: `protected + "." + base64url(payload)`.
184    pub signing_input: Vec<u8>,
185    /// Optional unprotected JOSE header.
186    pub unprotected_header: Option<JsonObject>,
187}
188
189/// Errors produced by agent-card signature helper APIs.
190#[derive(Debug, Error)]
191pub enum AgentCardSignatureError {
192    /// The agent card does not contain any signatures.
193    #[error("agent card does not contain any signatures")]
194    MissingSignatures,
195    /// The signature protected header could not be base64url-decoded.
196    #[error("invalid protected header encoding: {0}")]
197    InvalidProtectedEncoding(String),
198    /// The signature bytes could not be base64url-decoded.
199    #[error("invalid signature encoding: {0}")]
200    InvalidSignatureEncoding(String),
201    /// The protected header JSON is malformed.
202    #[error("invalid protected header JSON: {0}")]
203    InvalidProtectedHeader(#[source] serde_json::Error),
204    /// No signature used a caller-supported algorithm.
205    #[error("no agent-card signature matched the supported algorithms")]
206    UnsupportedAlgorithm,
207    /// All candidate signatures failed caller-supplied verification.
208    #[error("agent-card signature verification failed")]
209    VerificationFailed,
210    /// JSON serialization needed for canonicalization failed.
211    #[error("agent-card serialization failed: {0}")]
212    Serialization(#[from] serde_json::Error),
213}
214
215impl AgentCard {
216    /// Return a clone of the card with signature blocks removed.
217    pub fn unsigned_clone(&self) -> Self {
218        let mut card = self.clone();
219        card.signatures.clear();
220        card
221    }
222
223    /// Canonicalize the unsigned agent card for detached-JWS verification.
224    pub fn canonical_signing_payload(&self) -> Result<String, AgentCardSignatureError> {
225        canonicalize_json(&serde_json::to_value(self.unsigned_clone())?)
226    }
227
228    /// Verify any advertised signature using caller-supplied crypto.
229    ///
230    /// The caller controls key lookup and cryptographic verification. The SDK
231    /// prepares detached-JWS inputs and filters signatures by supported
232    /// algorithm identifiers.
233    pub fn verify_signatures<F>(
234        &self,
235        supported_algorithms: &[&str],
236        mut verifier: F,
237    ) -> Result<(), AgentCardSignatureError>
238    where
239        F: FnMut(&AgentCardSignatureVerificationInput) -> Result<bool, AgentCardSignatureError>,
240    {
241        if self.signatures.is_empty() {
242            return Err(AgentCardSignatureError::MissingSignatures);
243        }
244
245        let mut matched_algorithm = false;
246        for signature in &self.signatures {
247            let input = signature.verification_input(self)?;
248            if !supported_algorithms.is_empty()
249                && !supported_algorithms.iter().any(|algorithm| {
250                    algorithm.eq_ignore_ascii_case(input.protected_header.alg.as_str())
251                })
252            {
253                continue;
254            }
255
256            matched_algorithm = true;
257            if verifier(&input)? {
258                return Ok(());
259            }
260        }
261
262        if !matched_algorithm {
263            return Err(AgentCardSignatureError::UnsupportedAlgorithm);
264        }
265
266        Err(AgentCardSignatureError::VerificationFailed)
267    }
268}
269
270impl AgentCardSignature {
271    /// Decode the protected JOSE header from its base64url segment.
272    pub fn protected_header(&self) -> Result<JwsProtectedHeader, AgentCardSignatureError> {
273        let bytes = base64_url_engine()
274            .decode(self.protected.as_bytes())
275            .map_err(|error| {
276                AgentCardSignatureError::InvalidProtectedEncoding(error.to_string())
277            })?;
278
279        serde_json::from_slice(&bytes).map_err(AgentCardSignatureError::InvalidProtectedHeader)
280    }
281
282    /// Decode the raw signature bytes from their base64url representation.
283    pub fn signature_bytes(&self) -> Result<Vec<u8>, AgentCardSignatureError> {
284        base64_url_engine()
285            .decode(self.signature.as_bytes())
286            .map_err(|error| AgentCardSignatureError::InvalidSignatureEncoding(error.to_string()))
287    }
288
289    /// Build the detached-JWS verification input for this signature.
290    pub fn verification_input(
291        &self,
292        card: &AgentCard,
293    ) -> Result<AgentCardSignatureVerificationInput, AgentCardSignatureError> {
294        let protected_header = self.protected_header()?;
295        let signature = self.signature_bytes()?;
296        let payload = card.canonical_signing_payload()?;
297        let payload_segment = base64_url_engine().encode(payload.as_bytes());
298        let signing_input = format!("{}.{}", self.protected, payload_segment).into_bytes();
299
300        Ok(AgentCardSignatureVerificationInput {
301            protected_header,
302            protected_segment: self.protected.clone(),
303            signature,
304            signing_input,
305            unprotected_header: self.header.clone(),
306        })
307    }
308}
309
310fn canonicalize_json(value: &Value) -> Result<String, AgentCardSignatureError> {
311    match value {
312        Value::Null => Ok("null".to_owned()),
313        Value::Bool(value) => Ok(if *value { "true" } else { "false" }.to_owned()),
314        Value::Number(value) => Ok(value.to_string()),
315        Value::String(value) => serde_json::to_string(value).map_err(AgentCardSignatureError::from),
316        Value::Array(values) => {
317            let mut json = String::from("[");
318            for (index, value) in values.iter().enumerate() {
319                if index > 0 {
320                    json.push(',');
321                }
322                json.push_str(&canonicalize_json(value)?);
323            }
324            json.push(']');
325            Ok(json)
326        }
327        Value::Object(values) => {
328            let mut keys = values.keys().collect::<Vec<_>>();
329            keys.sort_unstable();
330
331            let mut json = String::from("{");
332            for (index, key) in keys.into_iter().enumerate() {
333                if index > 0 {
334                    json.push(',');
335                }
336                json.push_str(&serde_json::to_string(key)?);
337                json.push(':');
338                json.push_str(&canonicalize_json(&values[key])?);
339            }
340            json.push('}');
341            Ok(json)
342        }
343    }
344}
345
346fn base64_url_engine() -> &'static base64::engine::GeneralPurpose {
347    &base64::engine::general_purpose::URL_SAFE_NO_PAD
348}
349
350#[cfg(test)]
351mod tests {
352    use std::collections::BTreeMap;
353
354    use super::{
355        AgentCapabilities, AgentCard, AgentCardSignature, AgentCardSignatureError, AgentExtension,
356        AgentInterface, AgentSkill, JwsProtectedHeader,
357    };
358    use base64::Engine as _;
359    use serde_json::json;
360
361    #[test]
362    fn agent_card_round_trip_serialization() {
363        let card = AgentCard {
364            name: "Echo Agent".to_owned(),
365            description: "Replies with the same text".to_owned(),
366            supported_interfaces: vec![AgentInterface {
367                url: "https://example.com/rpc".to_owned(),
368                protocol_binding: "JSONRPC".to_owned(),
369                tenant: None,
370                protocol_version: "1.0".to_owned(),
371            }],
372            provider: None,
373            version: "0.1.0".to_owned(),
374            documentation_url: None,
375            capabilities: AgentCapabilities {
376                streaming: Some(true),
377                push_notifications: Some(false),
378                extensions: vec![AgentExtension {
379                    uri: "https://example.com/ext/streaming".to_owned(),
380                    description: "Streaming support".to_owned(),
381                    required: false,
382                    params: None,
383                }],
384                extended_agent_card: Some(false),
385            },
386            security_schemes: BTreeMap::new(),
387            security_requirements: Vec::new(),
388            default_input_modes: vec!["text/plain".to_owned()],
389            default_output_modes: vec!["text/plain".to_owned()],
390            skills: vec![AgentSkill {
391                id: "echo".to_owned(),
392                name: "Echo".to_owned(),
393                description: "Echo back user input".to_owned(),
394                tags: vec!["utility".to_owned()],
395                examples: vec!["echo hello".to_owned()],
396                input_modes: vec!["text/plain".to_owned()],
397                output_modes: vec!["text/plain".to_owned()],
398                security_requirements: Vec::new(),
399            }],
400            signatures: Vec::new(),
401            icon_url: None,
402        };
403
404        let json = serde_json::to_string(&card).expect("card should serialize");
405        let round_trip: AgentCard = serde_json::from_str(&json).expect("card should deserialize");
406
407        assert_eq!(round_trip.name, "Echo Agent");
408        assert_eq!(
409            round_trip.supported_interfaces[0].protocol_binding,
410            "JSONRPC"
411        );
412        assert_eq!(
413            round_trip.capabilities.extensions[0].description,
414            "Streaming support"
415        );
416        assert!(!round_trip.capabilities.extensions[0].required);
417        assert_eq!(round_trip.skills[0].id, "echo");
418    }
419
420    #[test]
421    fn signature_helper_decodes_protected_header() {
422        let protected = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
423            serde_json::to_vec(&json!({
424                "alg": "ES256",
425                "kid": "key-1",
426                "typ": "JOSE",
427            }))
428            .expect("header should serialize"),
429        );
430        let signature = AgentCardSignature {
431            protected,
432            signature: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([1_u8, 2, 3]),
433            header: None,
434        };
435
436        let header = signature
437            .protected_header()
438            .expect("protected header should decode");
439        assert_eq!(
440            header,
441            JwsProtectedHeader {
442                alg: "ES256".to_owned(),
443                kid: "key-1".to_owned(),
444                typ: Some("JOSE".to_owned()),
445                jku: None,
446                extra: BTreeMap::new(),
447            }
448        );
449    }
450
451    #[test]
452    fn canonical_signing_payload_omits_signatures() {
453        let mut card = sample_card();
454        card.signatures.push(sample_signature());
455
456        let payload = card
457            .canonical_signing_payload()
458            .expect("payload should canonicalize");
459
460        assert!(!payload.contains("\"signatures\""));
461        assert!(payload.starts_with("{\"capabilities\""));
462    }
463
464    #[test]
465    fn verify_signatures_builds_detached_jws_input() {
466        let mut card = sample_card();
467        let signature = sample_signature();
468        let protected = signature.protected.clone();
469        card.signatures.push(signature);
470
471        let payload_segment = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
472            card.canonical_signing_payload()
473                .expect("payload should canonicalize"),
474        );
475        let expected_input = format!("{protected}.{payload_segment}");
476
477        card.verify_signatures(&["ES256"], |input| {
478            assert_eq!(input.protected_header.alg, "ES256");
479            assert_eq!(input.protected_header.kid, "key-1");
480            assert_eq!(input.signature, vec![1_u8, 2, 3]);
481            assert_eq!(input.signing_input, expected_input.as_bytes());
482            Ok(true)
483        })
484        .expect("verification should succeed");
485    }
486
487    #[test]
488    fn verify_signatures_rejects_cards_without_supported_algorithms() {
489        let mut card = sample_card();
490        card.signatures.push(sample_signature());
491
492        let error = card
493            .verify_signatures(&["RS256"], |_input| Ok(true))
494            .expect_err("unsupported algorithms should fail");
495
496        assert!(matches!(
497            error,
498            AgentCardSignatureError::UnsupportedAlgorithm
499        ));
500    }
501
502    fn sample_card() -> AgentCard {
503        AgentCard {
504            name: "Echo Agent".to_owned(),
505            description: "Replies with the same text".to_owned(),
506            supported_interfaces: vec![AgentInterface {
507                url: "https://example.com/rpc".to_owned(),
508                protocol_binding: "JSONRPC".to_owned(),
509                tenant: None,
510                protocol_version: "1.0".to_owned(),
511            }],
512            provider: None,
513            version: "0.1.0".to_owned(),
514            documentation_url: None,
515            capabilities: AgentCapabilities {
516                streaming: Some(true),
517                push_notifications: Some(false),
518                extensions: vec![AgentExtension {
519                    uri: "https://example.com/ext/streaming".to_owned(),
520                    description: "Streaming support".to_owned(),
521                    required: false,
522                    params: None,
523                }],
524                extended_agent_card: Some(false),
525            },
526            security_schemes: BTreeMap::new(),
527            security_requirements: Vec::new(),
528            default_input_modes: vec!["text/plain".to_owned()],
529            default_output_modes: vec!["text/plain".to_owned()],
530            skills: vec![AgentSkill {
531                id: "echo".to_owned(),
532                name: "Echo".to_owned(),
533                description: "Echo back user input".to_owned(),
534                tags: vec!["utility".to_owned()],
535                examples: vec!["echo hello".to_owned()],
536                input_modes: vec!["text/plain".to_owned()],
537                output_modes: vec!["text/plain".to_owned()],
538                security_requirements: Vec::new(),
539            }],
540            signatures: Vec::new(),
541            icon_url: None,
542        }
543    }
544
545    fn sample_signature() -> AgentCardSignature {
546        AgentCardSignature {
547            protected: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
548                serde_json::to_vec(&json!({
549                    "alg": "ES256",
550                    "kid": "key-1",
551                    "typ": "JOSE",
552                }))
553                .expect("header should serialize"),
554            ),
555            signature: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([1_u8, 2, 3]),
556            header: None,
557        }
558    }
559}