Skip to main content

acp_runtime/
overlay.rs

1// Copyright 2026 ACP Project
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5use std::sync::Arc;
6
7use serde_json::{Map, Value};
8
9use crate::agent::{AcpAgent, InboundHandlerFn};
10use crate::errors::{AcpError, AcpResult, FailReason};
11use crate::messages::{DeliveryMode, MessageClass, SendResult};
12
13pub type BusinessHandler =
14    Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>;
15pub type PassthroughHandler =
16    Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct OverlayTarget {
20    pub agent_id: String,
21    pub base_url: String,
22    pub well_known_url: String,
23    pub identity_document_url: String,
24}
25
26#[derive(Debug, Clone)]
27pub struct OverlaySendResult {
28    pub target: Option<OverlayTarget>,
29    pub send_result: SendResult,
30}
31
32#[derive(Clone)]
33pub struct OverlayInboundAdapter {
34    pub agent: AcpAgent,
35    pub business_handler: BusinessHandler,
36    pub passthrough_handler: Option<PassthroughHandler>,
37}
38
39impl OverlayInboundAdapter {
40    pub fn new(
41        agent: AcpAgent,
42        business_handler: BusinessHandler,
43        passthrough_handler: Option<PassthroughHandler>,
44    ) -> Self {
45        Self {
46            agent,
47            business_handler,
48            passthrough_handler,
49        }
50    }
51
52    pub fn handle_request(&mut self, body: &Map<String, Value>) -> AcpResult<Map<String, Value>> {
53        if !is_acp_http_message(body) {
54            if let Some(passthrough) = &self.passthrough_handler {
55                let payload = passthrough(body).unwrap_or_default();
56                let mut response = Map::new();
57                response.insert("mode".to_string(), Value::String("passthrough".to_string()));
58                response.insert("payload".to_string(), Value::Object(payload));
59                return Ok(response);
60            }
61            return Err(AcpError::Validation(
62                "Request is not an ACP message and no passthrough_handler is configured"
63                    .to_string(),
64            ));
65        }
66        let business_handler = self.business_handler.clone();
67        let inbound_handler =
68            move |payload: &Map<String, Value>, _envelope: &crate::messages::Envelope| {
69                business_handler(payload)
70            };
71        let inbound = self
72            .agent
73            .receive(body, Some(&inbound_handler as &InboundHandlerFn));
74        let mut response = Map::new();
75        response.insert("mode".to_string(), Value::String("acp".to_string()));
76        response.insert(
77            "acp_result".to_string(),
78            serde_json::to_value(&inbound).unwrap_or(Value::Null),
79        );
80        response.insert(
81            "state".to_string(),
82            Value::String(format!("{:?}", inbound.state).to_uppercase()),
83        );
84        if let Some(reason_code) = inbound.reason_code {
85            response.insert("reason_code".to_string(), Value::String(reason_code));
86        } else {
87            response.insert("reason_code".to_string(), Value::Null);
88        }
89        if let Some(detail) = inbound.detail {
90            response.insert("detail".to_string(), Value::String(detail));
91        } else {
92            response.insert("detail".to_string(), Value::Null);
93        }
94        response.insert(
95            "response_message".to_string(),
96            inbound
97                .response_message
98                .map(Value::Object)
99                .unwrap_or(Value::Null),
100        );
101        Ok(response)
102    }
103}
104
105#[derive(Clone)]
106pub struct OverlayOutboundAdapter {
107    pub agent: AcpAgent,
108}
109
110impl OverlayOutboundAdapter {
111    pub fn new(agent: AcpAgent) -> Self {
112        Self { agent }
113    }
114
115    pub fn resolve_target(
116        &mut self,
117        target_base_url: &str,
118        expected_agent_id: Option<&str>,
119    ) -> AcpResult<OverlayTarget> {
120        let resolved = self
121            .agent
122            .resolve_well_known(target_base_url, expected_agent_id)?;
123        let well_known = resolved
124            .get("well_known")
125            .and_then(Value::as_object)
126            .ok_or_else(|| {
127                AcpError::Discovery(
128                    "Resolved well-known metadata missing well_known object".to_string(),
129                )
130            })?;
131        let identity_document = resolved
132            .get("identity_document")
133            .and_then(Value::as_object)
134            .ok_or_else(|| {
135                AcpError::Discovery(
136                    "Resolved well-known metadata missing identity_document object".to_string(),
137                )
138            })?;
139        let agent_id = identity_document
140            .get("agent_id")
141            .and_then(Value::as_str)
142            .map(str::trim)
143            .filter(|v| !v.is_empty())
144            .ok_or_else(|| {
145                AcpError::Discovery(
146                    "Resolved well-known metadata did not include a valid identity_document.agent_id"
147                        .to_string(),
148                )
149            })?;
150        let identity_document_url = well_known
151            .get("identity_document")
152            .and_then(Value::as_str)
153            .map(str::trim)
154            .filter(|v| !v.is_empty())
155            .ok_or_else(|| {
156                AcpError::Discovery(
157                    "Resolved well-known metadata did not include a valid identity_document URL"
158                        .to_string(),
159                )
160            })?;
161        Ok(OverlayTarget {
162            agent_id: agent_id.to_string(),
163            base_url: target_base_url.trim_end_matches('/').to_string(),
164            well_known_url: resolved
165                .get("well_known_url")
166                .and_then(Value::as_str)
167                .unwrap_or_default()
168                .to_string(),
169            identity_document_url: identity_document_url.to_string(),
170        })
171    }
172
173    #[allow(clippy::too_many_arguments)]
174    pub fn send_business_payload(
175        &mut self,
176        payload: Map<String, Value>,
177        target_base_url: Option<&str>,
178        recipient_agent_id: Option<&str>,
179        context: Option<String>,
180        delivery_mode: Option<DeliveryMode>,
181        expires_in_seconds: i64,
182    ) -> AcpResult<OverlaySendResult> {
183        let mut target = None;
184        let mut resolved_recipient = recipient_agent_id.map(str::to_string);
185        if let Some(target_base_url) = target_base_url {
186            let resolved_target = self.resolve_target(target_base_url, recipient_agent_id)?;
187            if resolved_recipient.is_none() {
188                resolved_recipient = Some(resolved_target.agent_id.clone());
189            }
190            target = Some(resolved_target);
191        }
192        let recipient = resolved_recipient.ok_or_else(|| {
193            AcpError::Validation(
194                "send_business_payload requires recipient_agent_id or target_base_url for well-known bootstrap"
195                    .to_string(),
196            )
197        })?;
198        let send_result = self.agent.send(
199            vec![recipient],
200            payload,
201            context,
202            MessageClass::Send,
203            expires_in_seconds,
204            None,
205            None,
206            delivery_mode,
207        )?;
208        Ok(OverlaySendResult {
209            target,
210            send_result,
211        })
212    }
213}
214
215pub fn is_acp_http_message(body: &Map<String, Value>) -> bool {
216    body.get("envelope").and_then(Value::as_object).is_some()
217        && body.get("protected").and_then(Value::as_object).is_some()
218}
219
220pub fn invalid_overlay_request(detail: impl Into<String>) -> Map<String, Value> {
221    let mut response = Map::new();
222    response.insert("mode".to_string(), Value::String("invalid".to_string()));
223    response.insert("state".to_string(), Value::String("FAILED".to_string()));
224    response.insert(
225        "reason_code".to_string(),
226        Value::String(FailReason::PolicyRejected.as_str().to_string()),
227    );
228    response.insert("detail".to_string(), Value::String(detail.into()));
229    response.insert("response_message".to_string(), Value::Null);
230    response
231}