Skip to main content

acp_runtime/
overlay_framework.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;
10use crate::errors::{AcpError, AcpResult};
11use crate::messages::DeliveryMode;
12use crate::overlay::{
13    BusinessHandler, OverlayInboundAdapter, OverlayOutboundAdapter, PassthroughHandler,
14    invalid_overlay_request,
15};
16
17pub const WELL_KNOWN_CACHE_CONTROL: &str = "public, max-age=300";
18
19#[derive(Debug, Clone)]
20pub struct OverlayHttpResponse {
21    pub status_code: u16,
22    pub body: Map<String, Value>,
23}
24
25#[derive(Clone)]
26pub struct OverlayFrameworkRuntime {
27    pub agent: AcpAgent,
28    pub base_url: String,
29    pub inbound_adapter: OverlayInboundAdapter,
30    pub outbound_adapter: OverlayOutboundAdapter,
31}
32
33#[derive(Clone)]
34pub struct OverlayConfig {
35    pub agent: AcpAgent,
36    pub base_url: String,
37    pub passthrough_handler: Option<PassthroughHandler>,
38}
39
40#[derive(Clone)]
41pub struct OverlayClient {
42    pub agent: AcpAgent,
43    pub outbound_adapter: OverlayOutboundAdapter,
44}
45
46impl OverlayFrameworkRuntime {
47    pub fn create(
48        agent: AcpAgent,
49        base_url: &str,
50        business_handler: BusinessHandler,
51        passthrough_handler: Option<PassthroughHandler>,
52    ) -> AcpResult<Self> {
53        let normalized_base_url = base_url.trim();
54        if normalized_base_url.is_empty() {
55            return Err(AcpError::Validation("base_url is required".to_string()));
56        }
57        let inbound_adapter =
58            OverlayInboundAdapter::new(agent.clone(), business_handler, passthrough_handler);
59        let outbound_adapter = OverlayOutboundAdapter::new(agent.clone());
60        Ok(Self {
61            agent,
62            base_url: normalized_base_url.trim_end_matches('/').to_string(),
63            inbound_adapter,
64            outbound_adapter,
65        })
66    }
67
68    pub fn handle_message_body(&mut self, body: &Value) -> OverlayHttpResponse {
69        let Some(body) = body.as_object() else {
70            return OverlayHttpResponse {
71                status_code: 400,
72                body: invalid_overlay_request("Expected JSON object request body"),
73            };
74        };
75        match self.inbound_adapter.handle_request(body) {
76            Ok(response) => OverlayHttpResponse {
77                status_code: 200,
78                body: response,
79            },
80            Err(exc) => OverlayHttpResponse {
81                status_code: 400,
82                body: invalid_overlay_request(exc.to_string()),
83            },
84        }
85    }
86
87    pub fn well_known_document(&self) -> AcpResult<Map<String, Value>> {
88        self.agent
89            .build_well_known_document(Some(&self.base_url), None)
90    }
91
92    pub fn well_known_headers() -> Map<String, Value> {
93        let mut headers = Map::new();
94        headers.insert(
95            "Cache-Control".to_string(),
96            Value::String(WELL_KNOWN_CACHE_CONTROL.to_string()),
97        );
98        headers
99    }
100
101    pub fn identity_document_payload(&self) -> Map<String, Value> {
102        let mut payload = Map::new();
103        payload.insert(
104            "identity_document".to_string(),
105            Value::Object(self.agent.identity_document.clone()),
106        );
107        payload
108    }
109
110    #[allow(clippy::too_many_arguments)]
111    pub fn send_business_payload(
112        &mut self,
113        payload: Map<String, Value>,
114        target_base_url: Option<&str>,
115        recipient_agent_id: Option<&str>,
116        context: Option<String>,
117        delivery_mode: Option<DeliveryMode>,
118        expires_in_seconds: i64,
119    ) -> AcpResult<Map<String, Value>> {
120        let result = self.outbound_adapter.send_business_payload(
121            payload,
122            target_base_url,
123            recipient_agent_id,
124            context,
125            delivery_mode,
126            expires_in_seconds,
127        )?;
128        Ok(send_result_to_map(result))
129    }
130
131    pub fn send_acp(
132        &mut self,
133        target_url: &str,
134        payload: Map<String, Value>,
135        recipient_agent_id: Option<&str>,
136        context: Option<String>,
137        delivery_mode: Option<DeliveryMode>,
138        expires_in_seconds: i64,
139    ) -> AcpResult<Map<String, Value>> {
140        self.send_business_payload(
141            payload,
142            Some(target_url),
143            recipient_agent_id,
144            context,
145            delivery_mode,
146            expires_in_seconds,
147        )
148    }
149
150    pub fn handle(
151        request_body: &Value,
152        business_handler: BusinessHandler,
153        config: OverlayConfig,
154    ) -> OverlayHttpResponse {
155        let runtime = Self::create(
156            config.agent,
157            &config.base_url,
158            business_handler,
159            config.passthrough_handler,
160        );
161        match runtime {
162            Ok(mut runtime) => runtime.handle_message_body(request_body),
163            Err(exc) => OverlayHttpResponse {
164                status_code: 400,
165                body: invalid_overlay_request(exc.to_string()),
166            },
167        }
168    }
169}
170
171impl OverlayClient {
172    pub fn create(agent: AcpAgent) -> Self {
173        Self {
174            outbound_adapter: OverlayOutboundAdapter::new(agent.clone()),
175            agent,
176        }
177    }
178
179    #[allow(clippy::too_many_arguments)]
180    pub fn send_acp(
181        &mut self,
182        target_url: &str,
183        payload: Map<String, Value>,
184        recipient_agent_id: Option<&str>,
185        context: Option<String>,
186        delivery_mode: Option<DeliveryMode>,
187        expires_in_seconds: i64,
188    ) -> AcpResult<Map<String, Value>> {
189        let result = self.outbound_adapter.send_business_payload(
190            payload,
191            Some(target_url),
192            recipient_agent_id,
193            context,
194            delivery_mode,
195            expires_in_seconds,
196        )?;
197        Ok(send_result_to_map(result))
198    }
199}
200
201pub fn acp_overlay_inbound(
202    mut agent: AcpAgent,
203    handler: Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>,
204    passthrough: bool,
205) -> impl FnMut(&Map<String, Value>) -> AcpResult<Map<String, Value>> {
206    let passthrough_handler = if passthrough {
207        Some(handler.clone())
208    } else {
209        None
210    };
211    move |payload: &Map<String, Value>| {
212        let mut inbound =
213            OverlayInboundAdapter::new(agent.clone(), handler.clone(), passthrough_handler.clone());
214        let response = inbound.handle_request(payload);
215        agent = inbound.agent;
216        response
217    }
218}
219
220fn send_result_to_map(result: crate::overlay::OverlaySendResult) -> Map<String, Value> {
221    let mut map = Map::new();
222    if let Some(target) = result.target {
223        map.insert(
224            "target".to_string(),
225            serde_json::json!({
226                "agent_id": target.agent_id,
227                "base_url": target.base_url,
228                "well_known_url": target.well_known_url,
229                "identity_document_url": target.identity_document_url,
230            }),
231        );
232    } else {
233        map.insert("target".to_string(), Value::Null);
234    }
235    map.insert(
236        "send_result".to_string(),
237        serde_json::to_value(&result.send_result).unwrap_or(Value::Null),
238    );
239    map
240}