Skip to main content

a2a_protocol_types/
agent_card.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! Agent card and capability discovery types.
5//!
6//! The [`AgentCard`] is the root discovery document served by an A2A agent at
7//! `/.well-known/agent.json`. It describes the agent's identity,
8//! capabilities, skills, security requirements, and supported interfaces.
9//!
10//! # v1.0 changes
11//!
12//! - `url` and `preferred_transport` replaced by `supported_interfaces`
13//! - `protocol_version` moved from `AgentCard` to `AgentInterface`
14//! - `AgentInterface.transport` renamed to `protocol_binding`
15//! - `supports_authenticated_extended_card` moved to `AgentCapabilities.extended_agent_card`
16//! - Security fields renamed to `security_requirements`
17
18use serde::{Deserialize, Serialize};
19
20use crate::extensions::{AgentCardSignature, AgentExtension};
21use crate::security::{NamedSecuritySchemes, SecurityRequirement};
22
23// ── AgentInterface ────────────────────────────────────────────────────────────
24
25/// A transport interface offered by an agent.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct AgentInterface {
29    /// Base URL of this interface endpoint.
30    pub url: String,
31
32    /// Protocol binding identifier (e.g. `"JSONRPC"`, `"REST"`, `"GRPC"`).
33    pub protocol_binding: String,
34
35    /// A2A protocol version string (e.g. `"1.0.0"`).
36    pub protocol_version: String,
37
38    /// Optional tenant identifier for multi-tenancy.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub tenant: Option<String>,
41}
42
43// ── AgentCapabilities ─────────────────────────────────────────────────────────
44
45/// Optional capability flags advertised by an agent.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48#[non_exhaustive]
49pub struct AgentCapabilities {
50    /// Whether the agent supports streaming via `SendStreamingMessage`.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub streaming: Option<bool>,
53
54    /// Whether the agent supports push notification delivery.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub push_notifications: Option<bool>,
57
58    /// Whether this agent serves an authenticated extended card.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub extended_agent_card: Option<bool>,
61
62    /// Optional extensions supported by this agent.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub extensions: Option<Vec<AgentExtension>>,
65}
66
67impl AgentCapabilities {
68    /// Creates an [`AgentCapabilities`] with all flags unset.
69    #[must_use]
70    pub const fn none() -> Self {
71        Self {
72            streaming: None,
73            push_notifications: None,
74            extended_agent_card: None,
75            extensions: None,
76        }
77    }
78
79    /// Sets the streaming capability flag.
80    #[must_use]
81    pub const fn with_streaming(mut self, streaming: bool) -> Self {
82        self.streaming = Some(streaming);
83        self
84    }
85
86    /// Sets the push notifications capability flag.
87    #[must_use]
88    pub const fn with_push_notifications(mut self, push: bool) -> Self {
89        self.push_notifications = Some(push);
90        self
91    }
92
93    /// Sets the extended agent card capability flag.
94    #[must_use]
95    pub const fn with_extended_agent_card(mut self, extended: bool) -> Self {
96        self.extended_agent_card = Some(extended);
97        self
98    }
99}
100
101impl Default for AgentCapabilities {
102    fn default() -> Self {
103        Self::none()
104    }
105}
106
107// ── AgentProvider ─────────────────────────────────────────────────────────────
108
109/// The organization that operates or publishes the agent.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct AgentProvider {
113    /// Name of the organization.
114    pub organization: String,
115
116    /// URL of the organization's website.
117    pub url: String,
118}
119
120// ── AgentSkill ────────────────────────────────────────────────────────────────
121
122/// A discrete capability offered by an agent.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct AgentSkill {
126    /// Unique skill identifier within the agent.
127    pub id: String,
128
129    /// Human-readable skill name.
130    pub name: String,
131
132    /// Human-readable description of what the skill does.
133    pub description: String,
134
135    /// Searchable tags for the skill.
136    pub tags: Vec<String>,
137
138    /// Example prompts illustrating how to invoke the skill.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub examples: Option<Vec<String>>,
141
142    /// MIME types accepted as input by this skill.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub input_modes: Option<Vec<String>>,
145
146    /// MIME types produced as output by this skill.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub output_modes: Option<Vec<String>>,
149
150    /// Security requirements specific to this skill.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub security_requirements: Option<Vec<SecurityRequirement>>,
153}
154
155// ── AgentCard ─────────────────────────────────────────────────────────────────
156
157/// The root discovery document for an A2A agent.
158///
159/// Served at `/.well-known/agent.json`. Clients fetch this document to
160/// discover the agent's interfaces, capabilities, skills, and security
161/// requirements before establishing a session.
162///
163/// In v1.0, `protocol_version` and `url` moved to [`AgentInterface`], and
164/// `supported_interfaces` replaces the old `url`/`preferred_transport`/
165/// `additional_interfaces` fields.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct AgentCard {
169    /// Display name of the agent.
170    pub name: String,
171
172    /// Human-readable description of the agent's purpose.
173    pub description: String,
174
175    /// Semantic version of this agent implementation.
176    pub version: String,
177
178    /// Transport interfaces offered by this agent.
179    ///
180    /// **Spec requirement:** Must contain at least one element.
181    pub supported_interfaces: Vec<AgentInterface>,
182
183    /// Default MIME types accepted as input.
184    pub default_input_modes: Vec<String>,
185
186    /// Default MIME types produced as output.
187    pub default_output_modes: Vec<String>,
188
189    /// Skills offered by this agent.
190    ///
191    /// **Spec requirement:** Must contain at least one element.
192    pub skills: Vec<AgentSkill>,
193
194    /// Capability flags.
195    pub capabilities: AgentCapabilities,
196
197    /// The organization operating this agent.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub provider: Option<AgentProvider>,
200
201    /// URL of the agent's icon image.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub icon_url: Option<String>,
204
205    /// URL of the agent's documentation.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub documentation_url: Option<String>,
208
209    /// Named security scheme definitions (OpenAPI-style).
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub security_schemes: Option<NamedSecuritySchemes>,
212
213    /// Global security requirements for the agent.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub security_requirements: Option<Vec<SecurityRequirement>>,
216
217    /// Cryptographic signatures over this card.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub signatures: Option<Vec<AgentCardSignature>>,
220}
221
222// ── Tests ─────────────────────────────────────────────────────────────────────
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn minimal_card() -> AgentCard {
229        AgentCard {
230            name: "Test Agent".into(),
231            description: "A test agent".into(),
232            version: "1.0.0".into(),
233            supported_interfaces: vec![AgentInterface {
234                url: "https://agent.example.com/rpc".into(),
235                protocol_binding: "JSONRPC".into(),
236                protocol_version: "1.0.0".into(),
237                tenant: None,
238            }],
239            default_input_modes: vec!["text/plain".into()],
240            default_output_modes: vec!["text/plain".into()],
241            skills: vec![AgentSkill {
242                id: "echo".into(),
243                name: "Echo".into(),
244                description: "Echoes input".into(),
245                tags: vec!["echo".into()],
246                examples: None,
247                input_modes: None,
248                output_modes: None,
249                security_requirements: None,
250            }],
251            capabilities: AgentCapabilities::none(),
252            provider: None,
253            icon_url: None,
254            documentation_url: None,
255            security_schemes: None,
256            security_requirements: None,
257            signatures: None,
258        }
259    }
260
261    #[test]
262    fn agent_card_roundtrip() {
263        let card = minimal_card();
264        let json = serde_json::to_string(&card).expect("serialize");
265        assert!(json.contains("\"supportedInterfaces\""));
266        assert!(json.contains("\"protocolBinding\":\"JSONRPC\""));
267        assert!(json.contains("\"protocolVersion\":\"1.0.0\""));
268        assert!(
269            !json.contains("\"preferredTransport\""),
270            "v1.0 removed this field"
271        );
272
273        let back: AgentCard = serde_json::from_str(&json).expect("deserialize");
274        assert_eq!(back.name, "Test Agent");
275        assert_eq!(back.supported_interfaces[0].protocol_binding, "JSONRPC");
276    }
277
278    #[test]
279    fn optional_fields_omitted() {
280        let card = minimal_card();
281        let json = serde_json::to_string(&card).expect("serialize");
282        assert!(!json.contains("\"provider\""), "provider should be absent");
283        assert!(!json.contains("\"iconUrl\""), "iconUrl should be absent");
284        assert!(
285            !json.contains("\"securitySchemes\""),
286            "securitySchemes should be absent"
287        );
288    }
289
290    #[test]
291    fn extended_agent_card_in_capabilities() {
292        let mut card = minimal_card();
293        card.capabilities.extended_agent_card = Some(true);
294        let json = serde_json::to_string(&card).expect("serialize");
295        assert!(json.contains("\"extendedAgentCard\":true"));
296    }
297
298    #[test]
299    fn wire_format_security_requirements_field_name() {
300        use crate::security::{SecurityRequirement, StringList};
301        use std::collections::HashMap;
302
303        let mut card = minimal_card();
304        card.security_requirements = Some(vec![SecurityRequirement {
305            schemes: HashMap::from([("bearer".into(), StringList { list: vec![] })]),
306        }]);
307        let json = serde_json::to_string(&card).unwrap();
308        // Must use "securityRequirements" (not "security")
309        assert!(
310            json.contains("\"securityRequirements\""),
311            "field must be securityRequirements: {json}"
312        );
313        assert!(
314            !json.contains("\"security\":"),
315            "must not have bare 'security' field: {json}"
316        );
317    }
318
319    #[test]
320    fn wire_format_skill_security_requirements() {
321        use crate::security::{SecurityRequirement, StringList};
322        use std::collections::HashMap;
323
324        let skill = AgentSkill {
325            id: "s1".into(),
326            name: "Skill".into(),
327            description: "A skill".into(),
328            tags: vec![],
329            examples: None,
330            input_modes: None,
331            output_modes: None,
332            security_requirements: Some(vec![SecurityRequirement {
333                schemes: HashMap::from([(
334                    "oauth2".into(),
335                    StringList {
336                        list: vec!["read".into()],
337                    },
338                )]),
339            }]),
340        };
341        let json = serde_json::to_string(&skill).unwrap();
342        assert!(
343            json.contains("\"securityRequirements\""),
344            "skill must use securityRequirements: {json}"
345        );
346    }
347
348    #[test]
349    fn wire_format_capabilities_no_state_transition_history() {
350        let card = minimal_card();
351        let json = serde_json::to_string(&card).unwrap();
352        assert!(
353            !json.contains("stateTransitionHistory"),
354            "stateTransitionHistory must not appear: {json}"
355        );
356    }
357}