Skip to main content

rustev_remote_http/
identity.rs

1//! Adapter binding identity (spec 009, 3.2.3) and served-model identity
2//! (3.7, I-2).
3
4use 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
15/// The tag of an adapter binding's digest.
16pub const BINDING_SCHEMA: &str = "rustev.remote-binding/1";
17
18/// Every output-affecting input of a remote adapter (3.2.3). Its digest is
19/// the adapter's `ArtifactId`, so a change to any of them changes the
20/// descriptor identity and the `PlanId` of every plan compiled against the
21/// adapter. Credentials, network addresses, concurrency and cost rates are
22/// configuration, never members.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24pub struct AdapterBinding {
25    /// The wire protocol and its version, for example `rustev.remote/1`.
26    pub protocol: String,
27    /// A class naming the endpoint, never a secret URL or key.
28    pub endpoint_class: String,
29    /// The remote backend's own artifact identity, when it reports one.
30    pub remote_artifact: Option<ArtifactId>,
31    /// The remote backend's descriptor identity, when it reports one.
32    pub remote_descriptor: Option<DescriptorId>,
33    /// Exactly the model the adapter asks for, and any version pin.
34    pub requested_model: Option<String>,
35    pub model_pin: Option<String>,
36    /// The question mapping's version and any mapping tables.
37    pub mapping_version: String,
38    pub mapping_tables: BTreeMap<String, String>,
39    /// Privacy options that can change routing, and so output (3.8.2).
40    pub routing_privacy: BTreeMap<String, String>,
41    pub max_request_bytes: u64,
42    pub max_response_bytes: u64,
43    /// The transport phase policy (spec 020, 3.1 and 3.4). `disabled` is
44    /// not written, so a binding with every spec 020 refinement disabled
45    /// keeps its spec 009 bytes and artifact.
46    #[serde(skip_serializing_if = "TransportPhases::is_disabled")]
47    pub transport_phases: TransportPhases,
48    /// The principal wire posture, mapping version and scope (spec 020,
49    /// 3.1 and 3.6); never a reference. Not written when omitted.
50    #[serde(skip_serializing_if = "PrincipalWire::is_omitted")]
51    pub principal_wire: PrincipalWire,
52    /// The run-record schema the evidence sink accepts, when it is
53    /// `rustev.run/2` (spec 020, 3.1 and 3.2). Not written otherwise.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub run_record_schema: Option<String>,
56    /// The most members of a runtime-planned batch this binding accepts
57    /// (spec 020, 3.1 and 3.3), when that dispatch is enabled. Not written
58    /// when disabled.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub batch_dispatch_max_members: Option<u32>,
61    /// The connection-pool policy and isolation-key schema (spec 020, 3.1
62    /// and 3.5). Not written when disabled.
63    #[serde(skip_serializing_if = "PoolBinding::is_disabled")]
64    pub connection_pool: PoolBinding,
65}
66
67impl AdapterBinding {
68    /// `sha256(BINDING_SCHEMA || 0x00 || canonical bytes)`, as an artifact.
69    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/// A served identity that contradicts a pinned one (3.7): the attempt fails
78/// `identity_mismatch` and its output is never supplied.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct IdentityMismatch {
81    pub pinned: String,
82    pub served: String,
83}
84
85/// The served identity to record, from the negotiated terms and what the
86/// remote side reported for this exchange.
87///
88/// - `pinned`: the pinned identity is recorded; a reported identity that
89///   differs is an [`IdentityMismatch`].
90/// - `reported`: a reported identity is recorded as `reported`; none is
91///   `unknown`.
92/// - `unknown`: always `unknown`. A name the remote side offers anyway is
93///   not a served identity under these terms; the caller keeps it among the
94///   provider extras. Nothing is promoted, and the requested model is never
95///   copied into the served field.
96pub 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        // Spec 020, 2: disabled refinements are not written, so the binding
172        // and the artifact of every existing adapter are unchanged.
173        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}