Skip to main content

acp_runtime/
capabilities.rs

1// Copyright 2026 ACP Project
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5use chrono::{Duration, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value};
8
9use crate::constants::{ACP_VERSION, DEFAULT_CRYPTO_SUITE};
10use crate::json_support::JsonMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AgentCapabilities {
14    #[serde(rename = "agent_id")]
15    pub agent_id: String,
16    #[serde(rename = "protocol_versions")]
17    pub protocol_versions: Vec<String>,
18    #[serde(rename = "crypto_suites")]
19    pub crypto_suites: Vec<String>,
20    pub transports: Vec<String>,
21    pub supports: JsonMap,
22    pub limits: JsonMap,
23    pub profiles: Vec<String>,
24    #[serde(rename = "valid_until")]
25    pub valid_until: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CapabilityMatch {
30    pub compatible: bool,
31    pub protocol_version: Option<String>,
32    pub crypto_suite: Option<String>,
33    pub transport: Option<String>,
34    pub reason: Option<String>,
35}
36
37impl CapabilityMatch {
38    pub fn compatible(protocol_version: String, crypto_suite: String, transport: String) -> Self {
39        Self {
40            compatible: true,
41            protocol_version: Some(protocol_version),
42            crypto_suite: Some(crypto_suite),
43            transport: Some(transport),
44            reason: None,
45        }
46    }
47
48    pub fn incompatible(reason: impl Into<String>) -> Self {
49        Self {
50            compatible: false,
51            protocol_version: None,
52            crypto_suite: None,
53            transport: None,
54            reason: Some(reason.into()),
55        }
56    }
57}
58
59impl AgentCapabilities {
60    pub fn new(agent_id: impl Into<String>) -> Self {
61        let mut supports = Map::new();
62        supports.insert("ack".to_string(), Value::Bool(true));
63        supports.insert("fail".to_string(), Value::Bool(true));
64        supports.insert("compensate".to_string(), Value::Bool(true));
65        supports.insert("direct_delivery".to_string(), Value::Bool(true));
66        supports.insert("relay_delivery".to_string(), Value::Bool(true));
67        supports.insert("amqp_delivery".to_string(), Value::Bool(true));
68        supports.insert("mqtt_delivery".to_string(), Value::Bool(true));
69
70        let mut limits = Map::new();
71        limits.insert(
72            "max_payload_bytes".to_string(),
73            Value::Number(1048576_u64.into()),
74        );
75
76        Self {
77            agent_id: agent_id.into(),
78            protocol_versions: vec![ACP_VERSION.to_string()],
79            crypto_suites: vec![DEFAULT_CRYPTO_SUITE.to_string()],
80            transports: vec![
81                "https".to_string(),
82                "http".to_string(),
83                "relay".to_string(),
84                "amqp".to_string(),
85                "mqtt".to_string(),
86            ],
87            supports,
88            limits,
89            profiles: vec!["core".to_string(), "self_asserted".to_string()],
90            valid_until: (Utc::now() + Duration::days(365)).to_rfc3339(),
91        }
92    }
93
94    pub fn from_map(value: Option<&JsonMap>, fallback_agent_id: &str) -> Self {
95        if let Some(raw) = value {
96            if let Ok(mut parsed) = serde_json::from_value::<Self>(Value::Object(raw.clone())) {
97                if parsed.agent_id.trim().is_empty() {
98                    parsed.agent_id = fallback_agent_id.to_string();
99                }
100                if parsed.protocol_versions.is_empty() {
101                    parsed.protocol_versions = vec![ACP_VERSION.to_string()];
102                }
103                if parsed.crypto_suites.is_empty() {
104                    parsed.crypto_suites = vec![DEFAULT_CRYPTO_SUITE.to_string()];
105                }
106                if parsed.transports.is_empty() {
107                    parsed.transports = vec![
108                        "https".to_string(),
109                        "http".to_string(),
110                        "relay".to_string(),
111                        "amqp".to_string(),
112                        "mqtt".to_string(),
113                    ];
114                }
115                return parsed;
116            }
117        }
118        Self::new(fallback_agent_id.to_string())
119    }
120
121    pub fn to_map(&self) -> JsonMap {
122        serde_json::to_value(self)
123            .ok()
124            .and_then(|v| v.as_object().cloned())
125            .unwrap_or_default()
126    }
127
128    pub fn choose_compatible(&self, remote: &AgentCapabilities) -> CapabilityMatch {
129        let protocol_version =
130            first_intersection(&self.protocol_versions, &remote.protocol_versions);
131        if protocol_version.is_none() {
132            return CapabilityMatch::incompatible("No compatible protocol version");
133        }
134        let crypto_suite = first_intersection(&self.crypto_suites, &remote.crypto_suites);
135        if crypto_suite.is_none() {
136            return CapabilityMatch::incompatible("No compatible crypto suite");
137        }
138        let transport = first_intersection(&self.transports, &remote.transports);
139        if transport.is_none() {
140            return CapabilityMatch::incompatible("No compatible transport");
141        }
142        CapabilityMatch::compatible(
143            protocol_version.unwrap_or_default(),
144            crypto_suite.unwrap_or_default(),
145            transport.unwrap_or_default(),
146        )
147    }
148}
149
150fn first_intersection(local: &[String], remote: &[String]) -> Option<String> {
151    for item in local {
152        if remote.iter().any(|candidate| candidate == item) {
153            return Some(item.clone());
154        }
155    }
156    None
157}