acp_runtime/
capabilities.rs1use 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}