acp-runtime 0.1.2

Rust SDK for the Agent Communication Protocol (ACP)
Documentation
// Copyright 2026 ACP Project
// Licensed under the Apache License, Version 2.0
// See LICENSE file for details.

use std::time::Duration;

use serde_json::{Map, Value};

use crate::errors::{AcpError, AcpResult};
use crate::http_security::{HttpSecurityPolicy, build_http_client, validate_http_url};
use crate::messages::AcpMessage;

#[derive(Debug, Clone)]
pub struct TransportResponse {
    pub status_code: u16,
    pub body: Option<Map<String, Value>>,
    pub raw_body: String,
}

#[derive(Debug, Clone)]
pub struct TransportClient {
    client: reqwest::blocking::Client,
    timeout_seconds: u64,
    allow_insecure_http: bool,
    mtls_enabled: bool,
}

impl TransportClient {
    pub fn new(timeout_seconds: u64, policy: &HttpSecurityPolicy) -> AcpResult<Self> {
        let client = build_http_client(timeout_seconds.max(1), policy)?;
        Ok(Self {
            client,
            timeout_seconds: timeout_seconds.max(1),
            allow_insecure_http: policy.allow_insecure_http,
            mtls_enabled: policy.mtls_enabled,
        })
    }

    pub fn post_json(&self, url: &str, body: &Map<String, Value>) -> AcpResult<TransportResponse> {
        validate_http_url(
            url,
            self.allow_insecure_http,
            self.mtls_enabled,
            "HTTP transport request",
        )?;
        let response = self
            .client
            .post(url)
            .header("Content-Type", "application/json")
            .timeout(Duration::from_secs(self.timeout_seconds))
            .json(body)
            .send()?;
        let status_code = response.status().as_u16();
        let raw_body = response.text().unwrap_or_default();
        let body = serde_json::from_str::<Value>(&raw_body)
            .ok()
            .and_then(|value| value.as_object().cloned());
        Ok(TransportResponse {
            status_code,
            body,
            raw_body,
        })
    }

    pub fn send_to_relay(
        &self,
        relay_url: &str,
        message: &AcpMessage,
    ) -> AcpResult<Map<String, Value>> {
        let relay_endpoint = if relay_url.ends_with('/') {
            format!("{relay_url}messages")
        } else {
            format!("{relay_url}/messages")
        };
        let response = self.post_json(&relay_endpoint, &message.to_map()?)?;
        if response.status_code != 200 {
            return Err(AcpError::Transport(format!(
                "Relay returned HTTP {} for message {}",
                response.status_code, message.envelope.message_id
            )));
        }
        response
            .body
            .ok_or_else(|| AcpError::Transport("Relay returned non-JSON response".to_string()))
    }
}