vex_api/a2a/
agent_card.rs

1//! A2A Agent Card
2//!
3//! The Agent Card is a JSON document that describes an agent's capabilities.
4//! It's served at `/.well-known/agent.json` per the A2A spec.
5//!
6//! # Security
7//!
8//! - Agent Cards can be protected via mTLS
9//! - Authentication requirements are declared in the card
10//! - Capabilities are whitelisted
11
12use serde::{Deserialize, Serialize};
13
14/// A2A Agent Card structure
15#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
16pub struct AgentCard {
17    /// Agent name (unique identifier)
18    pub name: String,
19    /// Human-readable description
20    pub description: String,
21    /// Protocol version
22    pub version: String,
23    /// Agent capabilities (skills)
24    pub skills: Vec<Skill>,
25    /// Authentication configuration
26    pub authentication: AuthConfig,
27    /// Provider information
28    pub provider: ProviderInfo,
29    /// Optional URL for agent documentation
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub docs_url: Option<String>,
32}
33
34/// A skill/capability that this agent offers
35#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
36pub struct Skill {
37    /// Skill identifier
38    pub id: String,
39    /// Human-readable name
40    pub name: String,
41    /// Description of what this skill does
42    pub description: String,
43    /// JSON Schema for skill input
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub input_schema: Option<serde_json::Value>,
46    /// JSON Schema for skill output
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub output_schema: Option<serde_json::Value>,
49}
50
51/// Authentication configuration for the agent
52#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
53pub struct AuthConfig {
54    /// Supported authentication schemes
55    pub schemes: Vec<String>,
56    /// OAuth 2.0 token endpoint (if applicable)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub token_endpoint: Option<String>,
59    /// OpenID Connect discovery URL
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub oidc_discovery: Option<String>,
62}
63
64/// Provider information
65#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
66pub struct ProviderInfo {
67    /// Organization name
68    pub organization: String,
69    /// Contact URL or email
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub contact: Option<String>,
72}
73
74impl AgentCard {
75    /// Create a new agent card with minimal info
76    pub fn new(name: impl Into<String>) -> Self {
77        Self {
78            name: name.into(),
79            description: String::new(),
80            version: "1.0".to_string(),
81            skills: Vec::new(),
82            authentication: AuthConfig::default(),
83            provider: ProviderInfo {
84                organization: "VEX".to_string(),
85                contact: None,
86            },
87            docs_url: None,
88        }
89    }
90
91    /// Set the agent description
92    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
93        self.description = desc.into();
94        self
95    }
96
97    /// Add a skill to the agent
98    pub fn with_skill(mut self, id: impl Into<String>, description: impl Into<String>) -> Self {
99        let id_str = id.into();
100        self.skills.push(Skill {
101            id: id_str.clone(),
102            name: id_str,
103            description: description.into(),
104            input_schema: None,
105            output_schema: None,
106        });
107        self
108    }
109
110    /// Add a skill with full details
111    pub fn with_skill_full(mut self, skill: Skill) -> Self {
112        self.skills.push(skill);
113        self
114    }
115
116    /// Set documentation URL
117    pub fn with_docs(mut self, url: impl Into<String>) -> Self {
118        self.docs_url = Some(url.into());
119        self
120    }
121
122    /// Set authentication config
123    pub fn with_auth(mut self, auth: AuthConfig) -> Self {
124        self.authentication = auth;
125        self
126    }
127
128    /// Create the default VEX agent card
129    pub fn vex_default() -> Self {
130        Self::new("vex-agent")
131            .with_description(
132                "VEX Protocol agent with adversarial verification and cryptographic proofs",
133            )
134            .with_skill("verify", "Verify a claim using adversarial red/blue debate")
135            .with_skill("hash", "Compute SHA-256 hash of content")
136            .with_skill("merkle_root", "Get current Merkle root for audit chain")
137            .with_docs("https://provnai.dev/docs")
138            .with_auth(AuthConfig {
139                schemes: vec!["bearer".to_string(), "api_key".to_string()],
140                token_endpoint: None,
141                oidc_discovery: None,
142            })
143    }
144}
145
146impl Default for AuthConfig {
147    fn default() -> Self {
148        Self {
149            schemes: vec!["bearer".to_string()],
150            token_endpoint: None,
151            oidc_discovery: None,
152        }
153    }
154}
155
156impl Skill {
157    /// Create a new skill
158    pub fn new(
159        id: impl Into<String>,
160        name: impl Into<String>,
161        description: impl Into<String>,
162    ) -> Self {
163        Self {
164            id: id.into(),
165            name: name.into(),
166            description: description.into(),
167            input_schema: None,
168            output_schema: None,
169        }
170    }
171
172    /// Add input schema
173    pub fn with_input_schema(mut self, schema: serde_json::Value) -> Self {
174        self.input_schema = Some(schema);
175        self
176    }
177
178    /// Add output schema
179    pub fn with_output_schema(mut self, schema: serde_json::Value) -> Self {
180        self.output_schema = Some(schema);
181        self
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_agent_card_new() {
191        let card = AgentCard::new("test-agent");
192        assert_eq!(card.name, "test-agent");
193        assert_eq!(card.version, "1.0");
194        assert!(card.skills.is_empty());
195    }
196
197    #[test]
198    fn test_agent_card_builder() {
199        let card = AgentCard::new("vex")
200            .with_description("VEX verifier")
201            .with_skill("verify", "Verify claims");
202
203        assert_eq!(card.description, "VEX verifier");
204        assert_eq!(card.skills.len(), 1);
205        assert_eq!(card.skills[0].id, "verify");
206    }
207
208    #[test]
209    fn test_vex_default() {
210        let card = AgentCard::vex_default();
211        assert_eq!(card.name, "vex-agent");
212        assert!(card.skills.len() >= 3);
213        assert!(card.docs_url.is_some());
214    }
215
216    #[test]
217    fn test_serialization() {
218        let card = AgentCard::vex_default();
219        let json = serde_json::to_string_pretty(&card).unwrap();
220        assert!(json.contains("vex-agent"));
221        assert!(json.contains("verify"));
222    }
223}