a3s-ahp 2.0.2

Agent Harness Protocol v2.0 — Universal, transport-agnostic protocol for supervising autonomous AI agents
Documentation
//! HTTP transport implementation

use crate::transport::TransportLayer;
use crate::{AhpError, AhpNotification, AhpRequest, AhpResponse, AuthConfig, AuthMethod, Result};
use async_trait::async_trait;
use reqwest::{header, Client};
use std::sync::Arc;

/// HTTP transport - communicates with remote harness server via HTTP
pub struct HttpTransport {
    client: Client,
    url: String,
    auth: Option<AuthConfig>,
}

impl HttpTransport {
    /// Create a new HTTP transport
    pub fn new(url: impl Into<String>, auth: Option<AuthConfig>) -> Result<Self> {
        let mut headers = header::HeaderMap::new();
        headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());

        // Add authentication headers
        if let Some(ref auth_config) = auth {
            match &auth_config.method {
                AuthMethod::ApiKey { key } => {
                    headers.insert(
                        "X-API-Key",
                        key.parse()
                            .map_err(|_| AhpError::AuthFailed("Invalid API key".to_string()))?,
                    );
                }
                AuthMethod::Bearer { token } => {
                    let auth_value = format!("Bearer {}", token);
                    headers.insert(
                        header::AUTHORIZATION,
                        auth_value.parse().map_err(|_| {
                            AhpError::AuthFailed("Invalid bearer token".to_string())
                        })?,
                    );
                }
                _ => {}
            }
        }

        let client = Client::builder()
            .default_headers(headers)
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| AhpError::Transport(format!("Failed to create HTTP client: {}", e)))?;

        Ok(Self {
            client,
            url: url.into(),
            auth,
        })
    }
}

#[async_trait]
impl TransportLayer for HttpTransport {
    async fn send_request(&self, request: AhpRequest) -> Result<AhpResponse> {
        let response = self
            .client
            .post(&self.url)
            .json(&request)
            .send()
            .await
            .map_err(|e| AhpError::Transport(format!("HTTP request failed: {}", e)))?;

        if !response.status().is_success() {
            return Err(AhpError::Transport(format!(
                "HTTP error: {} - {}",
                response.status(),
                response.text().await.unwrap_or_default()
            )));
        }

        let ahp_response: AhpResponse = response
            .json()
            .await
            .map_err(|e| AhpError::Protocol(format!("Failed to parse response: {}", e)))?;

        Ok(ahp_response)
    }

    async fn send_notification(&self, notification: AhpNotification) -> Result<()> {
        // For HTTP, notifications are sent as POST requests but we don't wait for response
        let _ = self
            .client
            .post(&self.url)
            .json(&notification)
            .send()
            .await
            .map_err(|e| AhpError::Transport(format!("HTTP notification failed: {}", e)))?;

        Ok(())
    }

    async fn close(&self) -> Result<()> {
        // HTTP client doesn't need explicit cleanup
        Ok(())
    }
}

/// HTTP server handler
pub struct HttpServer {
    server: Arc<crate::AhpServer>,
}

impl HttpServer {
    /// Create a new HTTP server
    pub fn new(server: Arc<crate::AhpServer>) -> Self {
        Self { server }
    }

    /// Run the HTTP server on the specified address
    #[cfg(feature = "http")]
    pub async fn run(self, addr: impl Into<std::net::SocketAddr>) -> Result<()> {
        use axum::{routing::post, Router};
        use tower_http::trace::TraceLayer;

        let app = Router::new()
            .route("/ahp", post(handle_request))
            .layer(TraceLayer::new_for_http())
            .with_state(self.server);

        let listener = tokio::net::TcpListener::bind(addr.into())
            .await
            .map_err(|e| AhpError::Transport(format!("Failed to bind: {}", e)))?;

        axum::serve(listener, app)
            .await
            .map_err(|e| AhpError::Transport(format!("Server error: {}", e)))?;

        Ok(())
    }
}

#[cfg(feature = "http")]
async fn handle_request(
    axum::extract::State(server): axum::extract::State<Arc<crate::AhpServer>>,
    axum::extract::Json(request): axum::extract::Json<AhpRequest>,
) -> axum::response::Json<AhpResponse> {
    let response = server.handle_request(request).await;
    axum::response::Json(response)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_http_transport_creation() {
        let transport = HttpTransport::new("http://localhost:8080/ahp", None);
        assert!(transport.is_ok());
    }

    #[test]
    fn test_http_transport_with_api_key() {
        let auth = Some(AuthConfig::api_key("test-key"));
        let transport = HttpTransport::new("http://localhost:8080/ahp", auth);
        assert!(transport.is_ok());
    }

    #[test]
    fn test_http_transport_with_bearer() {
        let auth = Some(AuthConfig::bearer("test-token"));
        let transport = HttpTransport::new("http://localhost:8080/ahp", auth);
        assert!(transport.is_ok());
    }
}