lean_ctx_protocol/
runtime_handshake.rs1use serde::{Deserialize, Serialize};
8
9pub const HANDSHAKE_SCHEMA: &str = "leanctx.runtime-handshake/v1";
10pub const WIRE_PROTOCOL: &str = "leanctx.protocol/v4";
11pub const MAX_HANDSHAKE_BYTES: usize = 64 * 1024;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct RuntimeHandshake {
15 pub schema: String,
16 pub wire_protocol: String,
17 pub runtime: String,
18 pub capabilities: Vec<String>,
19}
20
21impl RuntimeHandshake {
22 pub fn public(capabilities: impl IntoIterator<Item = String>) -> Self {
23 Self {
24 schema: HANDSHAKE_SCHEMA.to_owned(),
25 wire_protocol: WIRE_PROTOCOL.to_owned(),
26 runtime: "public-reference".to_owned(),
27 capabilities: capabilities.into_iter().collect(),
28 }
29 }
30
31 pub fn validate(&self) -> Result<(), String> {
32 if self.schema != HANDSHAKE_SCHEMA {
33 return Err(format!("unsupported handshake schema: {}", self.schema));
34 }
35 if self.wire_protocol != WIRE_PROTOCOL {
36 return Err(format!("unsupported wire protocol: {}", self.wire_protocol));
37 }
38 if self.runtime.trim().is_empty() {
39 return Err("runtime identity must not be empty".to_owned());
40 }
41 Ok(())
42 }
43
44 pub fn encode_frame(&self) -> Result<Vec<u8>, String> {
46 self.validate()?;
47 let payload = serde_json::to_vec(self).map_err(|error| error.to_string())?;
48 if payload.len() > MAX_HANDSHAKE_BYTES {
49 return Err("handshake exceeds maximum size".to_owned());
50 }
51 let length = u32::try_from(payload.len()).map_err(|_| "handshake too large".to_owned())?;
52 let mut frame = Vec::with_capacity(4 + payload.len());
53 frame.extend_from_slice(&length.to_be_bytes());
54 frame.extend_from_slice(&payload);
55 Ok(frame)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn legacy_public_frame_preserves_exact_wire_bytes() {
65 let value = RuntimeHandshake::public(["reference-planning".to_owned()]);
66 let payload = br#"{"schema":"leanctx.runtime-handshake/v1","wire_protocol":"leanctx.protocol/v4","runtime":"public-reference","capabilities":["reference-planning"]}"#;
67 let mut expected = u32::try_from(payload.len()).unwrap().to_be_bytes().to_vec();
68 expected.extend_from_slice(payload);
69 assert_eq!(value.encode_frame().unwrap(), expected);
70 assert_eq!(
71 serde_json::from_slice::<RuntimeHandshake>(payload).unwrap(),
72 value
73 );
74 }
75
76 #[test]
77 fn legacy_validation_rejects_wrong_versions_and_empty_identity() {
78 for (field, bad) in [
79 ("schema", "leanctx.runtime-handshake/v2"),
80 ("wire_protocol", "leanctx.protocol/v3"),
81 ("runtime", " "),
82 ] {
83 let mut json = serde_json::to_value(RuntimeHandshake::public([])).unwrap();
84 json[field] = serde_json::Value::String(bad.to_owned());
85 let value: RuntimeHandshake = serde_json::from_value(json).unwrap();
86 assert!(value.validate().is_err(), "{field}");
87 assert!(value.encode_frame().is_err(), "{field}");
88 }
89 }
90
91 #[test]
92 fn legacy_size_bound_is_on_encoded_payload_not_capability_count() {
93 let value = RuntimeHandshake::public(["x".repeat(MAX_HANDSHAKE_BYTES)]);
94 assert_eq!(
95 value.encode_frame().unwrap_err(),
96 "handshake exceeds maximum size"
97 );
98 }
99}