1#![forbid(unsafe_code)]
31
32use std::{error::Error, fmt, path::PathBuf};
33
34use serde::{Deserialize, Serialize};
35
36pub mod frame;
37pub mod manifest;
38pub mod session;
39
40pub use frame::{Frame, FrameBuildError};
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct BindIdentity {
45 pub project_root: PathBuf,
46 pub harness: String,
47 pub session: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(tag = "kind", rename_all = "snake_case")]
53pub enum Principal {
54 Reserved { module_id: String },
56 Direct,
58 Unverified,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(tag = "kind", rename_all = "snake_case")]
76pub enum RouteTarget {
77 ToolProvider {
78 module_id: String,
79 },
80 ManagementSurface {
81 module_id: String,
82 },
83 InternalService {
84 module_id: String,
85 service_id: String,
86 },
87}
88
89pub const PROTOCOL_VERSION: u8 = 1;
91
92pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
95
96pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
101
102pub const HEADER_LEN: usize = 17;
104
105pub const FROZEN_PREFIX_LEN: usize = 5;
109
110pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct ErrorBody {
120 pub code: String,
121 pub message: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126pub struct ModuleHelloBody {
127 pub manifest: manifest::ModuleManifest,
128 pub protocol_ver: u8,
129 #[serde(default)]
130 pub control_ops: Option<Vec<String>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub launch_nonce: Option<String>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct ModuleHelloAckBody {
145 pub negotiated_ver: u8,
146 pub subc_ops: Vec<String>,
147 pub subc_capabilities: Vec<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub storage: Option<serde_json::Value>,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[repr(u8)]
164pub enum FrameType {
165 Request = 0,
166 Response = 1,
167 Push = 2,
168 StreamData = 3,
169 StreamEnd = 4,
170 Error = 5,
171 Cancel = 6,
172 Ping = 7,
173 Pong = 8,
174 Hello = 9,
175 HelloAck = 10,
176 Goodbye = 11,
177}
178
179impl FrameType {
180 pub fn from_u8(b: u8) -> Option<Self> {
182 Some(match b {
183 0 => Self::Request,
184 1 => Self::Response,
185 2 => Self::Push,
186 3 => Self::StreamData,
187 4 => Self::StreamEnd,
188 5 => Self::Error,
189 6 => Self::Cancel,
190 7 => Self::Ping,
191 8 => Self::Pong,
192 9 => Self::Hello,
193 10 => Self::HelloAck,
194 11 => Self::Goodbye,
195 _ => return None,
196 })
197 }
198
199 pub fn is_pure_header(self) -> bool {
200 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207#[repr(u8)]
208pub enum Priority {
209 Passive = 0,
210 Interactive = 1,
211 Background = 2,
212}
213
214impl Priority {
215 fn from_bits(bits: u8) -> Option<Self> {
216 Some(match bits {
217 0 => Self::Passive,
218 1 => Self::Interactive,
219 2 => Self::Background,
220 _ => return None,
221 })
222 }
223}
224
225const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
228const FLAG_LAST: u8 = 0b0000_1000; const FLAG_RESERVED_MASK: u8 = 0b1111_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct Flags(pub u8);
234
235impl Flags {
236 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
238 let mut b = 0u8;
239 if binary {
240 b |= FLAG_BINARY;
241 }
242 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
243 if last {
244 b |= FLAG_LAST;
245 }
246 Flags(b)
247 }
248
249 pub fn is_binary(self) -> bool {
251 self.0 & FLAG_BINARY != 0
252 }
253
254 pub fn is_last(self) -> bool {
256 self.0 & FLAG_LAST != 0
257 }
258
259 pub fn priority(self) -> Option<Priority> {
261 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
262 }
263
264 pub fn has_reserved_bits(self) -> bool {
266 self.0 & FLAG_RESERVED_MASK != 0
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub struct EnvelopeHeader {
273 pub len: u32,
275 pub ver: u8,
277 pub ty: FrameType,
279 pub flags: Flags,
281 pub channel: u16,
283 pub corr: u64,
285}
286
287impl EnvelopeHeader {
288 pub fn encode(&self) -> [u8; HEADER_LEN] {
290 let mut buf = [0u8; HEADER_LEN];
291 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
292 buf[4] = self.ver;
293 buf[5] = self.ty as u8;
294 buf[6] = self.flags.0;
295 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
296 buf[9..17].copy_from_slice(&self.corr.to_le_bytes());
297 buf
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum DecodeError {
304 TooShortForPrefix { have: usize },
306 UnsupportedVersion { ver: u8 },
308 TooShortForHeader { have: usize, need: usize },
310 UnknownFrameType { byte: u8 },
312 ReservedFlagBits { flags: u8 },
314 ReservedPriorityBits { flags: u8 },
316 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
318}
319
320impl fmt::Display for DecodeError {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 match self {
323 Self::TooShortForPrefix { have } => {
324 write!(f, "header shorter than frozen prefix: have {have} bytes")
325 }
326 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
327 Self::TooShortForHeader { have, need } => {
328 write!(
329 f,
330 "header too short for version: have {have} bytes, need {need}"
331 )
332 }
333 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
334 Self::ReservedFlagBits { flags } => {
335 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
336 }
337 Self::ReservedPriorityBits { flags } => {
338 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
339 }
340 Self::PureHeaderFrameWithBody { ty, len } => {
341 write!(
342 f,
343 "pure-header frame {ty:?} declared non-zero body length {len}"
344 )
345 }
346 }
347 }
348}
349
350impl Error for DecodeError {}
351
352fn header_len_for_version(ver: u8) -> Option<usize> {
355 match ver {
356 1 => Some(HEADER_LEN),
357 _ => None,
358 }
359}
360
361pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
369 if bytes.len() < FROZEN_PREFIX_LEN {
370 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
371 }
372 let ver = bytes[4];
373 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
374 if bytes.len() < need {
375 return Err(DecodeError::TooShortForHeader {
376 have: bytes.len(),
377 need,
378 });
379 }
380
381 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
382 let ty =
383 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
384 let flags = Flags(bytes[6]);
385 if flags.has_reserved_bits() {
386 return Err(DecodeError::ReservedFlagBits { flags: bytes[6] });
387 }
388 if flags.priority().is_none() {
389 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
390 }
391 if ty.is_pure_header() && len != 0 {
392 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
393 }
394 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
395 let corr = u64::from_le_bytes([
396 bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
397 ]);
398
399 Ok(EnvelopeHeader {
400 len,
401 ver,
402 ty,
403 flags,
404 channel,
405 corr,
406 })
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
414 EnvelopeHeader {
415 len,
416 ver: PROTOCOL_VERSION,
417 ty,
418 flags,
419 channel,
420 corr,
421 }
422 }
423
424 #[test]
425 fn bind_identity_round_trips_json() {
426 let identity = BindIdentity {
427 project_root: PathBuf::from("/tmp/project"),
428 harness: "opencode".to_string(),
429 session: "session-1".to_string(),
430 };
431
432 let encoded = serde_json::to_vec(&identity).unwrap();
433 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
434
435 assert_eq!(decoded, identity);
436 }
437
438 #[test]
439 fn route_target_variants_round_trip_json() {
440 let targets = [
441 RouteTarget::ToolProvider {
442 module_id: "aft".to_string(),
443 },
444 RouteTarget::ManagementSurface {
445 module_id: "memory".to_string(),
446 },
447 RouteTarget::InternalService {
448 module_id: "bus".to_string(),
449 service_id: "dm".to_string(),
450 },
451 ];
452
453 for target in targets {
454 let encoded = serde_json::to_vec(&target).unwrap();
455 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
456 assert_eq!(decoded, target);
457 }
458 }
459
460 #[test]
461 fn error_body_round_trips_json() {
462 let body = ErrorBody {
463 code: "config_divergence".to_string(),
464 message: "active config differs".to_string(),
465 };
466
467 let encoded = serde_json::to_vec(&body).unwrap();
468 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
469
470 assert_eq!(decoded, body);
471 }
472
473 #[test]
474 fn round_trip_request() {
475 let h = hdr(
476 1234,
477 FrameType::Request,
478 Flags::new(false, Priority::Interactive, false),
479 42,
480 0xDEAD_BEEF_0000_0001,
481 );
482 let decoded = decode_header(&h.encode()).unwrap();
483 assert_eq!(h, decoded);
484 }
485
486 #[test]
487 fn round_trip_all_frame_types() {
488 for b in 0u8..=11 {
489 let ty = FrameType::from_u8(b).unwrap();
490 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
491 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
492 }
493 }
494
495 #[test]
496 fn pure_header_frame_has_zero_len() {
497 let h = hdr(
499 0,
500 FrameType::Cancel,
501 Flags::new(false, Priority::Passive, false),
502 7,
503 99,
504 );
505 let d = decode_header(&h.encode()).unwrap();
506 assert_eq!(d.len, 0);
507 assert_eq!(d.corr, 99);
508 }
509
510 #[test]
511 fn flags_round_trip() {
512 let f = Flags::new(true, Priority::Background, true);
513 assert!(f.is_binary());
514 assert!(f.is_last());
515 assert_eq!(f.priority(), Some(Priority::Background));
516 let h = hdr(8, FrameType::StreamData, f, 1, 1);
517 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
518 }
519
520 #[test]
521 fn little_endian_and_frozen_prefix_layout() {
522 let h = hdr(1, FrameType::Request, Flags(0), 0, 0);
524 let buf = h.encode();
525 assert_eq!(buf[0], 1);
526 assert_eq!(buf[1..4], [0, 0, 0]);
527 assert_eq!(buf[4], PROTOCOL_VERSION); assert_eq!(buf.len(), HEADER_LEN);
529 }
530
531 #[test]
532 fn reject_too_short_for_prefix() {
533 assert_eq!(
534 decode_header(&[0, 0, 0, 0]),
535 Err(DecodeError::TooShortForPrefix { have: 4 })
536 );
537 }
538
539 #[test]
540 fn reject_too_short_for_header() {
541 let mut b = [0u8; 10];
543 b[4] = PROTOCOL_VERSION;
544 assert_eq!(
545 decode_header(&b),
546 Err(DecodeError::TooShortForHeader { have: 10, need: 17 })
547 );
548 }
549
550 #[test]
551 fn reject_unsupported_version() {
552 let mut b = [0u8; HEADER_LEN];
553 b[4] = 2; assert_eq!(
555 decode_header(&b),
556 Err(DecodeError::UnsupportedVersion { ver: 2 })
557 );
558 }
559
560 #[test]
561 fn reject_unknown_frame_type() {
562 let mut b = [0u8; HEADER_LEN];
563 b[4] = PROTOCOL_VERSION;
564 b[5] = 99;
565 assert_eq!(
566 decode_header(&b),
567 Err(DecodeError::UnknownFrameType { byte: 99 })
568 );
569 }
570
571 #[test]
572 fn reject_reserved_flag_bits() {
573 let mut b = [0u8; HEADER_LEN];
574 b[4] = PROTOCOL_VERSION;
575 b[5] = FrameType::Request as u8;
576 b[6] = 0b1000_0000; assert_eq!(
578 decode_header(&b),
579 Err(DecodeError::ReservedFlagBits { flags: 0b1000_0000 })
580 );
581 }
582
583 #[test]
584 fn reject_reserved_priority_bits() {
585 let mut b = [0u8; HEADER_LEN];
586 b[4] = PROTOCOL_VERSION;
587 b[5] = FrameType::Request as u8;
588 b[6] = 0b0000_0110; assert_eq!(
590 decode_header(&b),
591 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
592 );
593 }
594
595 #[test]
596 fn reject_pure_header_frame_with_body_len() {
597 let h = hdr(
598 1,
599 FrameType::Ping,
600 Flags::new(false, Priority::Passive, false),
601 0,
602 1,
603 );
604 assert_eq!(
605 decode_header(&h.encode()),
606 Err(DecodeError::PureHeaderFrameWithBody {
607 ty: FrameType::Ping,
608 len: 1
609 })
610 );
611 }
612}