vex_api/a2a/
handler.rs

1//! A2A HTTP handlers
2//!
3//! Axum handlers for A2A protocol endpoints.
4//!
5//! # Endpoints
6//!
7//! - `GET /.well-known/agent.json` - Agent Card
8//! - `POST /a2a/tasks` - Create a new task
9//! - `GET /a2a/tasks/:id` - Get task status
10//!
11//! # Security
12//!
13//! - Authentication required for task endpoints
14//! - Agent Card is public but can be rate-limited
15//! - All responses include Merkle hashes
16
17use axum::{
18    extract::{Path, State},
19    http::StatusCode,
20    Json,
21};
22use std::sync::Arc;
23use uuid::Uuid;
24
25use super::agent_card::AgentCard;
26use super::task::{TaskRequest, TaskResponse};
27
28/// Shared state for A2A handlers
29pub struct A2aState {
30    /// The agent card for this VEX instance
31    pub agent_card: AgentCard,
32    // In a full implementation, this would include:
33    // - Task storage
34    // - ToolExecutor reference
35    // - AuditStore reference
36}
37
38impl Default for A2aState {
39    fn default() -> Self {
40        Self {
41            agent_card: AgentCard::vex_default(),
42        }
43    }
44}
45
46/// Handler: GET /.well-known/agent.json
47///
48/// Returns the A2A Agent Card describing this agent's capabilities.
49/// This endpoint is public per the A2A spec.
50pub async fn agent_card_handler(State(state): State<Arc<A2aState>>) -> Json<AgentCard> {
51    Json(state.agent_card.clone())
52}
53
54/// Handler: POST /a2a/tasks
55///
56/// Create a new task for execution.
57///
58/// # Security
59/// - Validates caller authentication
60/// - Checks nonce/timestamp for replay protection
61/// - Rate limits per caller agent
62pub async fn create_task_handler(
63    State(_state): State<Arc<A2aState>>,
64    Json(request): Json<TaskRequest>,
65) -> (StatusCode, Json<TaskResponse>) {
66    // Validate nonce/timestamp for replay protection
67    // In a full implementation, we'd check:
68    // 1. Nonce hasn't been seen before
69    // 2. Timestamp is within acceptable window (e.g., 5 minutes)
70
71    // For now, return a pending response
72    // In a full implementation, this would:
73    // 1. Queue the task for execution
74    // 2. Return immediately with pending status
75    // 3. Execute asynchronously
76
77    let response = TaskResponse::pending(request.id);
78    (StatusCode::ACCEPTED, Json(response))
79}
80
81/// Handler: GET /a2a/tasks/:id
82///
83/// Get the status of an existing task.
84pub async fn get_task_handler(
85    State(_state): State<Arc<A2aState>>,
86    Path(task_id): Path<Uuid>,
87) -> Result<Json<TaskResponse>, StatusCode> {
88    // In a full implementation, this would look up the task from storage
89    // For now, return a placeholder response
90
91    // Simulate task not found for non-existent tasks
92    // In reality, we'd query task storage
93    let response = TaskResponse::pending(task_id);
94    Ok(Json(response))
95}
96
97/// Build A2A routes for inclusion in the main router
98///
99/// # Example
100///
101/// ```ignore
102/// let app = Router::new()
103///     .merge(a2a_routes(a2a_state));
104/// ```
105pub fn a2a_routes(state: Arc<A2aState>) -> axum::Router {
106    use axum::routing::{get, post};
107
108    axum::Router::new()
109        .route("/.well-known/agent.json", get(agent_card_handler))
110        .route("/a2a/tasks", post(create_task_handler))
111        .route("/a2a/tasks/{id}", get(get_task_handler))
112        .with_state(state)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use axum::body::Body;
119    use axum::http::Request;
120    use tower::ServiceExt;
121
122    fn create_test_state() -> Arc<A2aState> {
123        Arc::new(A2aState::default())
124    }
125
126    #[tokio::test]
127    async fn test_agent_card_endpoint() {
128        let state = create_test_state();
129        let app = a2a_routes(state);
130
131        let response = app
132            .oneshot(
133                Request::builder()
134                    .uri("/.well-known/agent.json")
135                    .body(Body::empty())
136                    .unwrap(),
137            )
138            .await
139            .unwrap();
140
141        assert_eq!(response.status(), StatusCode::OK);
142    }
143
144    #[tokio::test]
145    async fn test_create_task_endpoint() {
146        let state = create_test_state();
147        let app = a2a_routes(state);
148
149        let task_req = TaskRequest::new("verify", serde_json::json!({"claim": "test"}));
150        let body = serde_json::to_string(&task_req).unwrap();
151
152        let response = app
153            .oneshot(
154                Request::builder()
155                    .method("POST")
156                    .uri("/a2a/tasks")
157                    .header("content-type", "application/json")
158                    .body(Body::from(body))
159                    .unwrap(),
160            )
161            .await
162            .unwrap();
163
164        assert_eq!(response.status(), StatusCode::ACCEPTED);
165    }
166
167    #[tokio::test]
168    async fn test_get_task_endpoint() {
169        let state = create_test_state();
170        let app = a2a_routes(state);
171        let task_id = Uuid::new_v4();
172
173        let response = app
174            .oneshot(
175                Request::builder()
176                    .uri(format!("/a2a/tasks/{}", task_id))
177                    .body(Body::empty())
178                    .unwrap(),
179            )
180            .await
181            .unwrap();
182
183        assert_eq!(response.status(), StatusCode::OK);
184    }
185}