1use std::collections::BTreeMap;
5
6use rustev_contract::canonical::{CanonicalError, canonical_bytes, tagged_digest};
7use rustev_contract::ids::{ArtifactId, DescriptorId};
8use rustev_contract::remote::{Disclosure, ServedIdentity, ServedTerms};
9use serde::Serialize;
10
11use crate::phases::TransportPhases;
12use crate::pool::PoolBinding;
13use crate::principal::PrincipalWire;
14
15pub const BINDING_SCHEMA: &str = "rustev.remote-binding/1";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24pub struct AdapterBinding {
25 pub protocol: String,
27 pub endpoint_class: String,
29 pub remote_artifact: Option<ArtifactId>,
31 pub remote_descriptor: Option<DescriptorId>,
33 pub requested_model: Option<String>,
35 pub model_pin: Option<String>,
36 pub mapping_version: String,
38 pub mapping_tables: BTreeMap<String, String>,
39 pub routing_privacy: BTreeMap<String, String>,
41 pub max_request_bytes: u64,
42 pub max_response_bytes: u64,
43 #[serde(skip_serializing_if = "TransportPhases::is_disabled")]
47 pub transport_phases: TransportPhases,
48 #[serde(skip_serializing_if = "PrincipalWire::is_omitted")]
51 pub principal_wire: PrincipalWire,
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub run_record_schema: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
60 pub batch_dispatch_max_members: Option<u32>,
61 #[serde(skip_serializing_if = "PoolBinding::is_disabled")]
64 pub connection_pool: PoolBinding,
65}
66
67impl AdapterBinding {
68 pub fn artifact(&self) -> Result<ArtifactId, CanonicalError> {
70 let digest = tagged_digest(BINDING_SCHEMA, &canonical_bytes(self)?);
71 ArtifactId::parse(&digest).map_err(|e| CanonicalError::Serialize {
72 message: e.to_string(),
73 })
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct IdentityMismatch {
81 pub pinned: String,
82 pub served: String,
83}
84
85pub fn served_identity(
97 terms: &ServedTerms,
98 reported: Option<&str>,
99) -> Result<ServedIdentity, IdentityMismatch> {
100 match terms.disclosure {
101 Disclosure::Pinned => {
102 let pinned = terms.identity.clone().unwrap_or_default();
103 match reported {
104 Some(r) if r != pinned => Err(IdentityMismatch {
105 pinned,
106 served: r.to_string(),
107 }),
108 _ => Ok(ServedIdentity {
109 identity: Some(pinned),
110 disclosure: Disclosure::Pinned,
111 }),
112 }
113 }
114 Disclosure::Reported => Ok(match reported {
115 Some(r) => ServedIdentity {
116 identity: Some(r.to_string()),
117 disclosure: Disclosure::Reported,
118 },
119 None => ServedIdentity::unknown(),
120 }),
121 Disclosure::Unknown => Ok(ServedIdentity::unknown()),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn terms(d: Disclosure, id: Option<&str>) -> ServedTerms {
130 ServedTerms {
131 disclosure: d,
132 identity: id.map(str::to_string),
133 }
134 }
135
136 #[test]
137 fn a_pinned_identity_must_match() {
138 let t = terms(Disclosure::Pinned, Some("m@1"));
139 assert_eq!(
140 served_identity(&t, Some("m@1")).unwrap().disclosure,
141 Disclosure::Pinned
142 );
143 assert_eq!(
144 served_identity(&t, None).unwrap().identity.as_deref(),
145 Some("m@1")
146 );
147 assert!(served_identity(&t, Some("m@2")).is_err());
148 }
149
150 #[test]
151 fn unknown_is_never_promoted() {
152 let t = terms(Disclosure::Unknown, None);
153 assert_eq!(
154 served_identity(&t, Some("m@9")).unwrap(),
155 ServedIdentity::unknown()
156 );
157 let t = terms(Disclosure::Reported, None);
158 let s = served_identity(&t, Some("m@9")).unwrap();
159 assert_eq!(
160 (s.identity.as_deref(), s.disclosure),
161 (Some("m@9"), Disclosure::Reported)
162 );
163 assert_eq!(
164 served_identity(&t, None).unwrap(),
165 ServedIdentity::unknown()
166 );
167 }
168
169 #[test]
170 fn a_binding_with_every_refinement_disabled_keeps_its_spec_009_bytes() {
171 let b = AdapterBinding {
174 protocol: "rustev.remote/1".into(),
175 endpoint_class: "synthetic-loopback".into(),
176 remote_artifact: None,
177 remote_descriptor: None,
178 requested_model: None,
179 model_pin: None,
180 mapping_version: "identity/1".into(),
181 mapping_tables: BTreeMap::new(),
182 routing_privacy: BTreeMap::new(),
183 max_request_bytes: 1,
184 max_response_bytes: 1,
185 transport_phases: TransportPhases::Disabled,
186 principal_wire: PrincipalWire::Omitted,
187 run_record_schema: None,
188 batch_dispatch_max_members: None,
189 connection_pool: PoolBinding::Disabled,
190 };
191 assert_eq!(
192 String::from_utf8(canonical_bytes(&b).unwrap()).unwrap(),
193 concat!(
194 r#"{"endpoint_class":"synthetic-loopback","mapping_tables":{},"#,
195 r#""mapping_version":"identity/1","max_request_bytes":1,"#,
196 r#""max_response_bytes":1,"model_pin":null,"protocol":"rustev.remote/1","#,
197 r#""remote_artifact":null,"remote_descriptor":null,"requested_model":null,"#,
198 r#""routing_privacy":{}}"#
199 )
200 );
201 }
202
203 #[test]
204 fn every_binding_member_changes_the_artifact() {
205 let base = AdapterBinding {
206 protocol: "rustev.remote/1".into(),
207 endpoint_class: "synthetic-loopback".into(),
208 remote_artifact: None,
209 remote_descriptor: None,
210 requested_model: None,
211 model_pin: None,
212 mapping_version: "identity/1".into(),
213 mapping_tables: BTreeMap::new(),
214 routing_privacy: BTreeMap::new(),
215 max_request_bytes: 1,
216 max_response_bytes: 1,
217 transport_phases: TransportPhases::Disabled,
218 principal_wire: PrincipalWire::Omitted,
219 run_record_schema: None,
220 batch_dispatch_max_members: None,
221 connection_pool: PoolBinding::Disabled,
222 };
223 let a = base.artifact().unwrap();
224 assert_eq!(a, base.clone().artifact().unwrap());
225 let variants: Vec<AdapterBinding> = vec![
226 AdapterBinding {
227 protocol: "x".into(),
228 ..base.clone()
229 },
230 AdapterBinding {
231 endpoint_class: "x".into(),
232 ..base.clone()
233 },
234 AdapterBinding {
235 requested_model: Some("m".into()),
236 ..base.clone()
237 },
238 AdapterBinding {
239 model_pin: Some("p".into()),
240 ..base.clone()
241 },
242 AdapterBinding {
243 mapping_version: "x".into(),
244 ..base.clone()
245 },
246 AdapterBinding {
247 mapping_tables: BTreeMap::from([("k".into(), "v".into())]),
248 ..base.clone()
249 },
250 AdapterBinding {
251 routing_privacy: BTreeMap::from([("k".into(), "v".into())]),
252 ..base.clone()
253 },
254 AdapterBinding {
255 max_request_bytes: 2,
256 ..base.clone()
257 },
258 AdapterBinding {
259 max_response_bytes: 2,
260 ..base.clone()
261 },
262 AdapterBinding {
263 transport_phases: TransportPhases::Enabled(crate::phases::PhasePolicy {
264 resolution_ms: 1,
265 connect_ms: 1,
266 secure_handshake_ms: 1,
267 request_write_ms: 1,
268 first_response_byte_ms: 1,
269 response_body_ms: 1,
270 cancellation_ack_ms: 1,
271 }),
272 ..base.clone()
273 },
274 AdapterBinding {
275 principal_wire: PrincipalWire::HostOperated {
276 mapping_version: "m/1".into(),
277 scope: "s".into(),
278 },
279 ..base.clone()
280 },
281 AdapterBinding {
282 run_record_schema: Some("rustev.run/2".into()),
283 ..base.clone()
284 },
285 AdapterBinding {
286 batch_dispatch_max_members: Some(4),
287 ..base.clone()
288 },
289 AdapterBinding {
290 connection_pool: PoolBinding::Enabled {
291 policy: crate::pool::PoolPolicy {
292 max_connections: 1,
293 max_per_origin: 1,
294 max_streams: 1,
295 max_idle: 1,
296 idle_lifetime_ms: 1,
297 max_lifetime_ms: 1,
298 max_uses: 1,
299 on_exhaustion: crate::pool::OnExhaustion::Refuse,
300 },
301 key_schema: crate::pool::POOL_KEY_SCHEMA.into(),
302 },
303 ..base.clone()
304 },
305 AdapterBinding {
306 principal_wire: PrincipalWire::HostOperated {
307 mapping_version: "m/2".into(),
308 scope: "s".into(),
309 },
310 ..base.clone()
311 },
312 ];
313 for v in variants {
314 assert_ne!(v.artifact().unwrap(), a, "{v:?}");
315 }
316 }
317}