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