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::sync::Arc;

use serde_json::{Map, Value};

use crate::agent::{AcpAgent, InboundHandlerFn};
use crate::errors::{AcpError, AcpResult, FailReason};
use crate::messages::{DeliveryMode, MessageClass, SendResult};

pub type BusinessHandler =
    Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>;
pub type PassthroughHandler =
    Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OverlayTarget {
    pub agent_id: String,
    pub base_url: String,
    pub well_known_url: String,
    pub identity_document_url: String,
}

#[derive(Debug, Clone)]
pub struct OverlaySendResult {
    pub target: Option<OverlayTarget>,
    pub send_result: SendResult,
}

#[derive(Clone)]
pub struct OverlayInboundAdapter {
    pub agent: AcpAgent,
    pub business_handler: BusinessHandler,
    pub passthrough_handler: Option<PassthroughHandler>,
}

impl OverlayInboundAdapter {
    pub fn new(
        agent: AcpAgent,
        business_handler: BusinessHandler,
        passthrough_handler: Option<PassthroughHandler>,
    ) -> Self {
        Self {
            agent,
            business_handler,
            passthrough_handler,
        }
    }

    pub fn handle_request(&mut self, body: &Map<String, Value>) -> AcpResult<Map<String, Value>> {
        if !is_acp_http_message(body) {
            if let Some(passthrough) = &self.passthrough_handler {
                let payload = passthrough(body).unwrap_or_default();
                let mut response = Map::new();
                response.insert("mode".to_string(), Value::String("passthrough".to_string()));
                response.insert("payload".to_string(), Value::Object(payload));
                return Ok(response);
            }
            return Err(AcpError::Validation(
                "Request is not an ACP message and no passthrough_handler is configured"
                    .to_string(),
            ));
        }
        let business_handler = self.business_handler.clone();
        let inbound_handler =
            move |payload: &Map<String, Value>, _envelope: &crate::messages::Envelope| {
                business_handler(payload)
            };
        let inbound = self
            .agent
            .receive(body, Some(&inbound_handler as &InboundHandlerFn));
        let mut response = Map::new();
        response.insert("mode".to_string(), Value::String("acp".to_string()));
        response.insert(
            "acp_result".to_string(),
            serde_json::to_value(&inbound).unwrap_or(Value::Null),
        );
        response.insert(
            "state".to_string(),
            Value::String(format!("{:?}", inbound.state).to_uppercase()),
        );
        if let Some(reason_code) = inbound.reason_code {
            response.insert("reason_code".to_string(), Value::String(reason_code));
        } else {
            response.insert("reason_code".to_string(), Value::Null);
        }
        if let Some(detail) = inbound.detail {
            response.insert("detail".to_string(), Value::String(detail));
        } else {
            response.insert("detail".to_string(), Value::Null);
        }
        response.insert(
            "response_message".to_string(),
            inbound
                .response_message
                .map(Value::Object)
                .unwrap_or(Value::Null),
        );
        Ok(response)
    }
}

#[derive(Clone)]
pub struct OverlayOutboundAdapter {
    pub agent: AcpAgent,
}

impl OverlayOutboundAdapter {
    pub fn new(agent: AcpAgent) -> Self {
        Self { agent }
    }

    pub fn resolve_target(
        &mut self,
        target_base_url: &str,
        expected_agent_id: Option<&str>,
    ) -> AcpResult<OverlayTarget> {
        let resolved = self
            .agent
            .resolve_well_known(target_base_url, expected_agent_id)?;
        let well_known = resolved
            .get("well_known")
            .and_then(Value::as_object)
            .ok_or_else(|| {
                AcpError::Discovery(
                    "Resolved well-known metadata missing well_known object".to_string(),
                )
            })?;
        let identity_document = resolved
            .get("identity_document")
            .and_then(Value::as_object)
            .ok_or_else(|| {
                AcpError::Discovery(
                    "Resolved well-known metadata missing identity_document object".to_string(),
                )
            })?;
        let agent_id = identity_document
            .get("agent_id")
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|v| !v.is_empty())
            .ok_or_else(|| {
                AcpError::Discovery(
                    "Resolved well-known metadata did not include a valid identity_document.agent_id"
                        .to_string(),
                )
            })?;
        let identity_document_url = well_known
            .get("identity_document")
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|v| !v.is_empty())
            .ok_or_else(|| {
                AcpError::Discovery(
                    "Resolved well-known metadata did not include a valid identity_document URL"
                        .to_string(),
                )
            })?;
        Ok(OverlayTarget {
            agent_id: agent_id.to_string(),
            base_url: target_base_url.trim_end_matches('/').to_string(),
            well_known_url: resolved
                .get("well_known_url")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .to_string(),
            identity_document_url: identity_document_url.to_string(),
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn send_business_payload(
        &mut self,
        payload: Map<String, Value>,
        target_base_url: Option<&str>,
        recipient_agent_id: Option<&str>,
        context: Option<String>,
        delivery_mode: Option<DeliveryMode>,
        expires_in_seconds: i64,
    ) -> AcpResult<OverlaySendResult> {
        let mut target = None;
        let mut resolved_recipient = recipient_agent_id.map(str::to_string);
        if let Some(target_base_url) = target_base_url {
            let resolved_target = self.resolve_target(target_base_url, recipient_agent_id)?;
            if resolved_recipient.is_none() {
                resolved_recipient = Some(resolved_target.agent_id.clone());
            }
            target = Some(resolved_target);
        }
        let recipient = resolved_recipient.ok_or_else(|| {
            AcpError::Validation(
                "send_business_payload requires recipient_agent_id or target_base_url for well-known bootstrap"
                    .to_string(),
            )
        })?;
        let send_result = self.agent.send(
            vec![recipient],
            payload,
            context,
            MessageClass::Send,
            expires_in_seconds,
            None,
            None,
            delivery_mode,
        )?;
        Ok(OverlaySendResult {
            target,
            send_result,
        })
    }
}

pub fn is_acp_http_message(body: &Map<String, Value>) -> bool {
    body.get("envelope").and_then(Value::as_object).is_some()
        && body.get("protected").and_then(Value::as_object).is_some()
}

pub fn invalid_overlay_request(detail: impl Into<String>) -> Map<String, Value> {
    let mut response = Map::new();
    response.insert("mode".to_string(), Value::String("invalid".to_string()));
    response.insert("state".to_string(), Value::String("FAILED".to_string()));
    response.insert(
        "reason_code".to_string(),
        Value::String(FailReason::PolicyRejected.as_str().to_string()),
    );
    response.insert("detail".to_string(), Value::String(detail.into()));
    response.insert("response_message".to_string(), Value::Null);
    response
}