Skip to main content

a2a_protocol_types/
extensions.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! Agent extension and card-signature types.
5//!
6//! Extensions allow agents to advertise optional capabilities beyond the core
7//! A2A v1.0 specification. [`AgentExtension`] is referenced by
8//! [`crate::agent_card::AgentCapabilities`].
9
10use serde::{Deserialize, Serialize};
11
12// ── AgentExtension ────────────────────────────────────────────────────────────
13
14/// Describes an optional extension that an agent supports.
15///
16/// Extensions are identified by a URI and may carry an arbitrary JSON
17/// parameter block understood by the extension spec.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct AgentExtension {
21    /// Unique URI identifying the extension (e.g. `"https://example.com/ext/v1"`).
22    pub uri: String,
23
24    /// Human-readable description of the extension's purpose.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub description: Option<String>,
27
28    /// Whether clients **must** support this extension to interact correctly.
29    ///
30    /// A value of `true` means the agent cannot operate meaningfully without
31    /// the client understanding this extension.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub required: Option<bool>,
34
35    /// Extension-specific parameters; structure is defined by the extension URI.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub params: Option<serde_json::Value>,
38}
39
40impl AgentExtension {
41    /// Creates a minimal [`AgentExtension`] with only a URI.
42    #[must_use]
43    pub fn new(uri: impl Into<String>) -> Self {
44        Self {
45            uri: uri.into(),
46            description: None,
47            required: None,
48            params: None,
49        }
50    }
51}
52
53// ── AgentCardSignature ────────────────────────────────────────────────────────
54
55/// A cryptographic signature over an [`crate::agent_card::AgentCard`].
56///
57/// In v1.0, this is a structured type with JWS-style fields.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct AgentCardSignature {
61    /// The JWS protected header (base64url-encoded).
62    pub protected: String,
63
64    /// The JWS signature (base64url-encoded).
65    pub signature: String,
66
67    /// Additional unprotected header parameters.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub header: Option<serde_json::Value>,
70}
71
72// ── Tests ─────────────────────────────────────────────────────────────────────
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn agent_extension_minimal_roundtrip() {
80        let ext = AgentExtension::new("https://example.com/ext/v1");
81        let json = serde_json::to_string(&ext).expect("serialize");
82        assert!(json.contains("\"uri\""));
83        assert!(
84            !json.contains("\"description\""),
85            "None fields must be omitted"
86        );
87
88        let back: AgentExtension = serde_json::from_str(&json).expect("deserialize");
89        assert_eq!(back.uri, "https://example.com/ext/v1");
90    }
91
92    #[test]
93    fn agent_extension_full_roundtrip() {
94        let mut ext = AgentExtension::new("https://example.com/ext/v1");
95        ext.description = Some("Cool extension".into());
96        ext.required = Some(true);
97        ext.params = Some(serde_json::json!({"version": 2}));
98
99        let json = serde_json::to_string(&ext).expect("serialize");
100        let back: AgentExtension = serde_json::from_str(&json).expect("deserialize");
101
102        assert_eq!(back.description.as_deref(), Some("Cool extension"));
103        assert_eq!(back.required, Some(true));
104    }
105}