Skip to main content

protocol_droid/domain/a2a/
extended_agent_card.rs

1//! A2A `GetExtendedAgentCard` typed envelopes.
2//!
3//! # What these are
4//!
5//! [`GetExtendedAgentCardRequest`] and
6//! [`GetExtendedAgentCardResponse`] are the
7//! typed shapes for the `GetExtendedAgentCard`
8//! JSON-RPC method. The method returns the
9//! authenticated agent's extended card — the
10//! version of the card that is only available
11//! after the client has authenticated with the
12//! agent.
13//!
14//! The A2A v1.0 spec splits the agent card into
15//! two reads: the *public* card (no
16//! authentication) served on the well-known
17//! path, and the *extended* card (requires
18//! authentication) returned by
19//! `GetExtendedAgentCard`. The public card is
20//! what a peer sees when it discovers the
21//! agent; the extended card is the same card
22//! plus the additional capabilities the agent
23//! does not advertise publicly (a paid tool, an
24//! admin endpoint, etc.).
25//!
26//! # Why the request is an empty struct
27//!
28//! The spec's `GetExtendedAgentCardRequest`
29//! has only a `tenant` field, which is a
30//! path parameter — not a wire field. A
31//! JSON-RPC request with no body has `params:
32//! null` or `params: {}` on the wire, and
33//! the Rust type that matches that is
34//! `GetExtendedAgentCardRequest` (an empty
35//! struct that serialises to `{}` by default).
36//!
37//! # Why the response is a transparent newtype
38//! around `AgentCard`
39//!
40//! The wire form is the `AgentCard` object
41//! directly — no wrapper. A newtype with
42//! `transparent` is the cheapest shape that
43//! keeps the type distinct from a raw
44//! `AgentCard` (and lets the consumer
45//! pattern-match on the response type if they
46//! want to).
47
48use std::collections::BTreeMap;
49
50use serde::{Deserialize, Serialize};
51
52use super::agent_card::AgentCard;
53
54/// A typed `GetExtendedAgentCard` request.
55/// The wire form is `{}` (the spec's request
56/// has no body fields).
57#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct GetExtendedAgentCardRequest {
59    /// A per-request metadata slot. The
60    /// spec does not name a metadata field
61    /// on the request, but a server that
62    /// wants to accept arbitrary
63    /// client-side context (a trace id,
64    /// a tenant override) can use this
65    /// map. Empty by default, absent on
66    /// the wire.
67    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
68    pub metadata: BTreeMap<String, serde_json::Value>,
69}
70
71impl GetExtendedAgentCardRequest {
72    /// Starts a new request.
73    #[must_use]
74    pub const fn new() -> Self {
75        Self {
76            metadata: BTreeMap::new(),
77        }
78    }
79}
80
81/// A typed `GetExtendedAgentCard` response.
82/// The wire form is the [`AgentCard`] object
83/// directly.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct GetExtendedAgentCardResponse(pub AgentCard);
87
88impl From<AgentCard> for GetExtendedAgentCardResponse {
89    fn from(card: AgentCard) -> Self {
90        Self(card)
91    }
92}
93
94impl std::ops::Deref for GetExtendedAgentCardResponse {
95    type Target = AgentCard;
96
97    fn deref(&self) -> &Self::Target {
98        &self.0
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{GetExtendedAgentCardRequest, GetExtendedAgentCardResponse};
105    use crate::domain::a2a::agent_card::AgentCardBuilder;
106
107    type TestError = Box<dyn std::error::Error>;
108
109    #[test]
110    fn request_serialises_as_empty_object() -> Result<(), TestError> {
111        let request = GetExtendedAgentCardRequest::new();
112        let value = serde_json::to_value(&request)?;
113        let obj = value
114            .as_object()
115            .ok_or("a GetExtendedAgentCardRequest must serialise to a JSON object")?;
116        assert!(obj.is_empty(), "expected `{{}}`, got {value}");
117        Ok(())
118    }
119
120    #[test]
121    fn response_serialises_as_card_object() -> Result<(), TestError> {
122        let interface = crate::domain::a2a::agent_card::AgentInterface {
123            url: url::Url::parse("https://example.com/a2a/v1")
124                .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?,
125            protocol_binding: crate::domain::a2a::agent_card::JSONRPC.clone(),
126            protocol_version: crate::domain::a2a::A2aVersion::V1_0,
127            tenant: None,
128        };
129        let card = AgentCardBuilder::new()
130            .with_name("test-agent")
131            .with_description("An A2A test agent.")
132            .with_version("0.1.0")
133            .with_interface(interface)
134            .with_input_mode("text/plain")
135            .with_output_mode("text/plain")
136            .build()
137            .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
138        let response = GetExtendedAgentCardResponse::from(card);
139        let value = serde_json::to_value(&response)?;
140        let obj = value
141            .as_object()
142            .ok_or("a GetExtendedAgentCardResponse must serialise to a JSON object")?;
143        // The wire form is the inner card
144        // — no wrapper. The card's `name`
145        // is the discriminator.
146        assert_eq!(
147            obj.get("name").and_then(serde_json::Value::as_str),
148            Some("test-agent")
149        );
150        Ok(())
151    }
152
153    #[test]
154    fn response_from_card_via_into_works() -> Result<(), TestError> {
155        let interface = crate::domain::a2a::agent_card::AgentInterface {
156            url: url::Url::parse("https://example.com/a2a/v1")
157                .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?,
158            protocol_binding: crate::domain::a2a::agent_card::JSONRPC.clone(),
159            protocol_version: crate::domain::a2a::A2aVersion::V1_0,
160            tenant: None,
161        };
162        let card = AgentCardBuilder::new()
163            .with_name("test-agent")
164            .with_description("An A2A test agent.")
165            .with_version("0.1.0")
166            .with_interface(interface)
167            .with_input_mode("text/plain")
168            .with_output_mode("text/plain")
169            .build()
170            .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
171        let response: GetExtendedAgentCardResponse = card.clone().into();
172        assert_eq!(response.0, card);
173        Ok(())
174    }
175}