Skip to main content

microsandbox_protocol/
message.rs

1//! Message envelope and type definitions for the agent protocol.
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4
5use crate::error::ProtocolResult;
6
7//--------------------------------------------------------------------------------------------------
8// Constants
9//--------------------------------------------------------------------------------------------------
10
11/// Current protocol version.
12pub const PROTOCOL_VERSION: u8 = 7;
13
14/// Frame flag: this is the last message for the given correlation ID.
15///
16/// Set on terminal message types such as `ExecExited`, `FsResponse`, and `TcpClosed`.
17pub const FLAG_TERMINAL: u8 = 0b0000_0001;
18
19/// Frame flag: this is the first message of a new session.
20///
21/// Set on session-initiating message types such as `ExecRequest`, `FsRequest`, and `TcpConnect`.
22pub const FLAG_SESSION_START: u8 = 0b0000_0010;
23
24/// Frame flag: this message requests sandbox shutdown.
25///
26/// Set on `Shutdown` messages. The sandbox-process relay uses this to trigger
27/// drain escalation (SIGTERM → SIGKILL) if the guest doesn't exit voluntarily.
28pub const FLAG_SHUTDOWN: u8 = 0b0000_0100;
29
30/// Size of the frame header fields that sit between the length prefix and the
31/// CBOR payload: `[id: u32 BE][flags: u8]` = 5 bytes.
32pub const FRAME_HEADER_SIZE: usize = 5;
33
34//--------------------------------------------------------------------------------------------------
35// Types
36//--------------------------------------------------------------------------------------------------
37
38/// The message envelope sent over the wire.
39///
40/// Each message contains a version, type, correlation ID, flags, and a CBOR payload.
41///
42/// Wire format: `[len: u32 BE][id: u32 BE][flags: u8][CBOR(v, t, p)]`
43///
44/// The `id` and `flags` fields live in the binary frame header (outside CBOR)
45/// so that relay intermediaries can route frames without CBOR parsing.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Message {
48    /// Protocol generation, echoed into the frame.
49    ///
50    /// This is the single protocol version axis (see `VERSIONING.md`), the same
51    /// number negotiated once at the handshake — not a second, message-local
52    /// version. It is carried here so a frame is self-describing for debugging
53    /// and telemetry; behavior is gated on the negotiated generation, not on
54    /// reading this field per message.
55    pub v: u8,
56
57    /// Message type.
58    pub t: MessageType,
59
60    /// Correlation ID used to associate requests with responses and
61    /// to identify exec sessions.
62    ///
63    /// Serialized in the binary frame header, not in CBOR.
64    #[serde(skip)]
65    pub id: u32,
66
67    /// Frame flags computed from the message type.
68    ///
69    /// Serialized in the binary frame header, not in CBOR.
70    #[serde(skip)]
71    pub flags: u8,
72
73    /// The CBOR-encoded payload bytes.
74    #[serde(with = "serde_bytes")]
75    pub p: Vec<u8>,
76}
77
78/// Identifies the type of a protocol message.
79///
80/// The `#[strum(serialize = ...)]` attribute on each variant is the single
81/// source for its wire string: [`as_str`](Self::as_str) and
82/// [`from_wire_str`](Self::from_wire_str) are derived from it, and
83/// [`strum::IntoEnumIterator`] yields every variant for exhaustive iteration
84/// (the schema snapshot) without a hand-maintained list.
85#[derive(
86    Debug,
87    Clone,
88    Copy,
89    PartialEq,
90    Eq,
91    Hash,
92    strum::IntoStaticStr,
93    strum::EnumString,
94    strum::EnumIter,
95)]
96pub enum MessageType {
97    /// Guest agent is ready.
98    #[strum(serialize = "core.ready")]
99    Ready,
100
101    /// Guest reports init context before user mounts.
102    #[strum(serialize = "core.init.resolved")]
103    InitResolved,
104
105    /// Host acknowledges init-context setup.
106    #[strum(serialize = "core.init.ack")]
107    InitAck,
108
109    /// Host requests shutdown.
110    #[strum(serialize = "core.shutdown")]
111    Shutdown,
112
113    /// Host relay reports that one SDK client disconnected.
114    #[strum(serialize = "core.relay.client.disconnected")]
115    RelayClientDisconnected,
116
117    /// Host asks the guest to synchronize `CLOCK_REALTIME`.
118    #[strum(serialize = "core.clock.sync")]
119    ClockSync,
120
121    /// Host checks whether the guest agent is reachable.
122    #[strum(serialize = "core.ping")]
123    Ping,
124
125    /// Guest confirms that the guest agent is reachable.
126    #[strum(serialize = "core.pong")]
127    Pong,
128
129    /// Host explicitly records sandbox activity.
130    #[strum(serialize = "core.touch")]
131    Touch,
132
133    /// Guest confirms that sandbox activity was recorded.
134    #[strum(serialize = "core.touched")]
135    Touched,
136
137    /// Peer reports a recoverable protocol-level error.
138    #[strum(serialize = "core.error")]
139    CoreError,
140
141    /// Host requests command execution.
142    #[strum(serialize = "core.exec.request")]
143    ExecRequest,
144
145    /// Guest confirms command started.
146    #[strum(serialize = "core.exec.started")]
147    ExecStarted,
148
149    /// Host sends stdin data.
150    #[strum(serialize = "core.exec.stdin")]
151    ExecStdin,
152
153    /// Guest reports that a prior `ExecStdin` write to the child's
154    /// stdin failed (e.g. the child closed its read end). Non-terminal:
155    /// the session continues and may still produce stdout/stderr and
156    /// an exit code.
157    #[strum(serialize = "core.exec.stdin.error")]
158    ExecStdinError,
159
160    /// Guest sends stdout data.
161    #[strum(serialize = "core.exec.stdout")]
162    ExecStdout,
163
164    /// Guest sends stderr data.
165    #[strum(serialize = "core.exec.stderr")]
166    ExecStderr,
167
168    /// Guest reports command exit.
169    #[strum(serialize = "core.exec.exited")]
170    ExecExited,
171
172    /// Guest reports command failed to spawn (binary not found,
173    /// permission denied, etc.). Distinct from `ExecExited` —
174    /// `ExecFailed` means the user code never ran. Terminal.
175    #[strum(serialize = "core.exec.failed")]
176    ExecFailed,
177
178    /// Host requests PTY resize.
179    #[strum(serialize = "core.exec.resize")]
180    ExecResize,
181
182    /// Host sends signal to process.
183    #[strum(serialize = "core.exec.signal")]
184    ExecSignal,
185
186    /// Host requests a filesystem operation.
187    #[strum(serialize = "core.fs.request")]
188    FsRequest,
189
190    /// Guest sends a terminal filesystem response.
191    #[strum(serialize = "core.fs.response")]
192    FsResponse,
193
194    /// Streaming file data chunk (bidirectional).
195    #[strum(serialize = "core.fs.data")]
196    FsData,
197
198    /// Host requests a TCP connection from inside the guest.
199    #[strum(serialize = "core.tcp.connect")]
200    TcpConnect,
201
202    /// Guest confirms that a TCP connection was opened.
203    #[strum(serialize = "core.tcp.connected")]
204    TcpConnected,
205
206    /// TCP stream data chunk (bidirectional).
207    #[strum(serialize = "core.tcp.data")]
208    TcpData,
209
210    /// One TCP stream side has closed its write half.
211    #[strum(serialize = "core.tcp.eof")]
212    TcpEof,
213
214    /// Host requests a TCP session close.
215    #[strum(serialize = "core.tcp.close")]
216    TcpClose,
217
218    /// Guest reports that a TCP session is closed. Terminal.
219    #[strum(serialize = "core.tcp.closed")]
220    TcpClosed,
221
222    /// Guest reports that a TCP session failed. Terminal.
223    #[strum(serialize = "core.tcp.failed")]
224    TcpFailed,
225
226    /// Host supplies one-shot guest bootstrap configuration.
227    #[strum(serialize = "core.bootstrap")]
228    Bootstrap,
229}
230
231//--------------------------------------------------------------------------------------------------
232// Methods
233//--------------------------------------------------------------------------------------------------
234
235impl Message {
236    /// Creates a new message with the current protocol version and raw payload bytes.
237    pub fn new(t: MessageType, id: u32, p: Vec<u8>) -> Self {
238        let flags = t.flags();
239        Self {
240            v: PROTOCOL_VERSION,
241            t,
242            id,
243            flags,
244            p,
245        }
246    }
247
248    /// Creates a new message by serializing the given payload to CBOR.
249    pub fn with_payload<T: Serialize>(
250        t: MessageType,
251        id: u32,
252        payload: &T,
253    ) -> ProtocolResult<Self> {
254        let mut p = Vec::new();
255        ciborium::into_writer(payload, &mut p)?;
256        let flags = t.flags();
257        Ok(Self {
258            v: PROTOCOL_VERSION,
259            t,
260            id,
261            flags,
262            p,
263        })
264    }
265
266    /// Deserializes the payload bytes into the given type.
267    pub fn payload<T: DeserializeOwned>(&self) -> ProtocolResult<T> {
268        Ok(ciborium::from_reader(&self.p[..])?)
269    }
270}
271
272impl MessageType {
273    /// Computes the frame flags byte for this message type.
274    pub fn flags(&self) -> u8 {
275        match self {
276            Self::Pong
277            | Self::Touched
278            | Self::CoreError
279            | Self::ExecExited
280            | Self::ExecFailed
281            | Self::FsResponse
282            | Self::TcpClosed
283            | Self::TcpFailed => FLAG_TERMINAL,
284            Self::ExecRequest | Self::FsRequest | Self::TcpConnect => FLAG_SESSION_START,
285            Self::Shutdown => FLAG_SHUTDOWN,
286            _ => 0,
287        }
288    }
289
290    /// The protocol generation that introduced this message type.
291    ///
292    /// A per-type label on the single protocol generation axis (see
293    /// `VERSIONING.md`), not a separate version counter. The send path gates on
294    /// it: a type whose generation exceeds the peer's negotiated generation is
295    /// rejected locally with a typed error instead of being sent to a peer that
296    /// cannot handle it, so only that one feature fails rather than the session.
297    ///
298    /// Core and exec types belong to the generation-1 baseline; they work on
299    /// every runtime we still talk to, including the pre-0.5 legacy one.
300    /// Filesystem streaming did not exist in the pre-0.5 legacy protocol
301    /// (generation 1), so the `Fs*` types require generation 2 or newer.
302    /// TCP forwarding was introduced in generation 4. `core.error` was
303    /// introduced in generation 5. Reachability checks and explicit idle
304    /// refreshes were introduced in generation 6.
305    ///
306    /// There is deliberately no wildcard arm: adding a new `MessageType` must
307    /// force a conscious choice of the generation that introduced it (and a
308    /// matching `PROTOCOL_VERSION` bump). Message types are append-only — never
309    /// lower or re-purpose an existing value.
310    pub fn min_protocol_version(&self) -> u8 {
311        match self {
312            Self::Ready
313            | Self::InitResolved
314            | Self::InitAck
315            | Self::Shutdown
316            | Self::RelayClientDisconnected
317            | Self::ClockSync
318            | Self::ExecRequest
319            | Self::ExecStarted
320            | Self::ExecStdin
321            | Self::ExecStdinError
322            | Self::ExecStdout
323            | Self::ExecStderr
324            | Self::ExecExited
325            | Self::ExecFailed
326            | Self::ExecResize
327            | Self::ExecSignal => 1,
328            Self::FsRequest | Self::FsResponse | Self::FsData => 2,
329            Self::CoreError => 5,
330            Self::Ping | Self::Pong | Self::Touch | Self::Touched => 6,
331            Self::Bootstrap => 7,
332            Self::TcpConnect
333            | Self::TcpConnected
334            | Self::TcpData
335            | Self::TcpEof
336            | Self::TcpClose
337            | Self::TcpClosed
338            | Self::TcpFailed => 4,
339        }
340    }
341
342    /// Whether a peer that speaks `peer_generation` is new enough to handle this
343    /// message type.
344    ///
345    /// The shared version-compatibility primitive for both directions. The host
346    /// gates its sends on it (`AgentClient::ensure_version_compat`); the guest
347    /// can gate a guest-initiated message the same way, reading the peer's
348    /// generation from the `v` field of the request that established the session.
349    /// See `VERSIONING.md`.
350    pub fn is_available_at(&self, peer_generation: u8) -> bool {
351        self.min_protocol_version() <= peer_generation
352    }
353
354    /// Returns the wire string representation.
355    ///
356    /// Backed by the per-variant `#[strum(serialize = ...)]` attribute, the
357    /// single source of truth for wire strings.
358    pub fn as_str(&self) -> &'static str {
359        (*self).into()
360    }
361
362    /// Parses a wire string into a message type, the inverse of
363    /// [`as_str`](Self::as_str). Returns `None` for an unknown string.
364    pub fn from_wire_str(s: &str) -> Option<Self> {
365        s.parse().ok()
366    }
367}
368
369//--------------------------------------------------------------------------------------------------
370// Trait Implementations
371//--------------------------------------------------------------------------------------------------
372
373impl Serialize for MessageType {
374    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
375    where
376        S: serde::Serializer,
377    {
378        serializer.serialize_str(self.as_str())
379    }
380}
381
382impl<'de> Deserialize<'de> for MessageType {
383    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
384    where
385        D: serde::Deserializer<'de>,
386    {
387        let s = String::deserialize(deserializer)?;
388        Self::from_wire_str(&s)
389            .ok_or_else(|| serde::de::Error::custom(format!("unknown message type: {s}")))
390    }
391}
392
393//--------------------------------------------------------------------------------------------------
394// Tests
395//--------------------------------------------------------------------------------------------------
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_message_type_roundtrip() {
403        let types = [
404            (MessageType::Bootstrap, "core.bootstrap"),
405            (MessageType::Ready, "core.ready"),
406            (MessageType::InitResolved, "core.init.resolved"),
407            (MessageType::InitAck, "core.init.ack"),
408            (MessageType::Shutdown, "core.shutdown"),
409            (
410                MessageType::RelayClientDisconnected,
411                "core.relay.client.disconnected",
412            ),
413            (MessageType::ClockSync, "core.clock.sync"),
414            (MessageType::Ping, "core.ping"),
415            (MessageType::Pong, "core.pong"),
416            (MessageType::Touch, "core.touch"),
417            (MessageType::Touched, "core.touched"),
418            (MessageType::CoreError, "core.error"),
419            (MessageType::ExecRequest, "core.exec.request"),
420            (MessageType::ExecStarted, "core.exec.started"),
421            (MessageType::ExecStdin, "core.exec.stdin"),
422            (MessageType::ExecStdinError, "core.exec.stdin.error"),
423            (MessageType::ExecStdout, "core.exec.stdout"),
424            (MessageType::ExecStderr, "core.exec.stderr"),
425            (MessageType::ExecExited, "core.exec.exited"),
426            (MessageType::ExecFailed, "core.exec.failed"),
427            (MessageType::ExecResize, "core.exec.resize"),
428            (MessageType::ExecSignal, "core.exec.signal"),
429            (MessageType::FsRequest, "core.fs.request"),
430            (MessageType::FsResponse, "core.fs.response"),
431            (MessageType::FsData, "core.fs.data"),
432            (MessageType::TcpConnect, "core.tcp.connect"),
433            (MessageType::TcpConnected, "core.tcp.connected"),
434            (MessageType::TcpData, "core.tcp.data"),
435            (MessageType::TcpEof, "core.tcp.eof"),
436            (MessageType::TcpClose, "core.tcp.close"),
437            (MessageType::TcpClosed, "core.tcp.closed"),
438            (MessageType::TcpFailed, "core.tcp.failed"),
439        ];
440
441        for (mt, expected_str) in &types {
442            assert_eq!(mt.as_str(), *expected_str);
443            assert_eq!(MessageType::from_wire_str(expected_str).unwrap(), *mt);
444        }
445    }
446
447    #[test]
448    fn test_message_type_serde_roundtrip() {
449        let types = [
450            MessageType::Bootstrap,
451            MessageType::Ready,
452            MessageType::InitResolved,
453            MessageType::InitAck,
454            MessageType::Shutdown,
455            MessageType::RelayClientDisconnected,
456            MessageType::ClockSync,
457            MessageType::Ping,
458            MessageType::Pong,
459            MessageType::Touch,
460            MessageType::Touched,
461            MessageType::CoreError,
462            MessageType::ExecRequest,
463            MessageType::ExecStarted,
464            MessageType::ExecStdin,
465            MessageType::ExecStdinError,
466            MessageType::ExecStdout,
467            MessageType::ExecStderr,
468            MessageType::ExecExited,
469            MessageType::ExecFailed,
470            MessageType::ExecResize,
471            MessageType::ExecSignal,
472            MessageType::FsRequest,
473            MessageType::FsResponse,
474            MessageType::FsData,
475            MessageType::TcpConnect,
476            MessageType::TcpConnected,
477            MessageType::TcpData,
478            MessageType::TcpEof,
479            MessageType::TcpClose,
480            MessageType::TcpClosed,
481            MessageType::TcpFailed,
482        ];
483
484        for mt in &types {
485            let mut buf = Vec::new();
486            ciborium::into_writer(mt, &mut buf).unwrap();
487            let decoded: MessageType = ciborium::from_reader(&buf[..]).unwrap();
488            assert_eq!(&decoded, mt);
489        }
490    }
491
492    #[test]
493    fn test_unknown_message_type() {
494        assert!(MessageType::from_wire_str("core.unknown").is_none());
495    }
496
497    #[test]
498    fn test_message_with_payload_roundtrip() {
499        use crate::exec::ExecExited;
500
501        let msg =
502            Message::with_payload(MessageType::ExecExited, 7, &ExecExited { code: 42 }).unwrap();
503
504        assert_eq!(msg.t, MessageType::ExecExited);
505        assert_eq!(msg.id, 7);
506        assert_eq!(msg.flags, FLAG_TERMINAL);
507
508        let payload: ExecExited = msg.payload().unwrap();
509        assert_eq!(payload.code, 42);
510    }
511
512    #[test]
513    fn test_message_type_flags() {
514        assert_eq!(MessageType::ExecExited.flags(), FLAG_TERMINAL);
515        assert_eq!(MessageType::ExecFailed.flags(), FLAG_TERMINAL);
516        assert_eq!(MessageType::FsResponse.flags(), FLAG_TERMINAL);
517        assert_eq!(MessageType::TcpClosed.flags(), FLAG_TERMINAL);
518        assert_eq!(MessageType::TcpFailed.flags(), FLAG_TERMINAL);
519        assert_eq!(MessageType::Pong.flags(), FLAG_TERMINAL);
520        assert_eq!(MessageType::Touched.flags(), FLAG_TERMINAL);
521        assert_eq!(MessageType::ExecRequest.flags(), FLAG_SESSION_START);
522        assert_eq!(MessageType::FsRequest.flags(), FLAG_SESSION_START);
523        assert_eq!(MessageType::TcpConnect.flags(), FLAG_SESSION_START);
524        assert_eq!(MessageType::Ready.flags(), 0);
525        assert_eq!(MessageType::Bootstrap.flags(), 0);
526        assert_eq!(MessageType::InitResolved.flags(), 0);
527        assert_eq!(MessageType::InitAck.flags(), 0);
528        assert_eq!(MessageType::Shutdown.flags(), FLAG_SHUTDOWN);
529        assert_eq!(MessageType::ClockSync.flags(), 0);
530        assert_eq!(MessageType::Ping.flags(), 0);
531        assert_eq!(MessageType::Touch.flags(), 0);
532        assert_eq!(MessageType::ExecStarted.flags(), 0);
533        assert_eq!(MessageType::ExecStdin.flags(), 0);
534        assert_eq!(MessageType::ExecStdout.flags(), 0);
535        assert_eq!(MessageType::ExecStderr.flags(), 0);
536        assert_eq!(MessageType::ExecResize.flags(), 0);
537        assert_eq!(MessageType::ExecSignal.flags(), 0);
538        assert_eq!(MessageType::FsData.flags(), 0);
539        assert_eq!(MessageType::TcpConnected.flags(), 0);
540        assert_eq!(MessageType::TcpData.flags(), 0);
541        assert_eq!(MessageType::TcpEof.flags(), 0);
542        assert_eq!(MessageType::TcpClose.flags(), 0);
543    }
544
545    #[test]
546    fn test_additive_fields_keep_old_and_new_compatible() {
547        // The core backward-compatibility guarantee from VERSIONING.md: a new,
548        // always-optional field is safe in both directions across a version skew.
549        use serde::{Deserialize, Serialize};
550
551        // A payload as it existed at an older generation.
552        #[derive(Serialize, Deserialize)]
553        struct Old {
554            a: u32,
555            b: u32,
556        }
557
558        // The same payload after a later generation added `c` (optional).
559        #[derive(Serialize, Deserialize, Debug, PartialEq)]
560        struct New {
561            a: u32,
562            b: u32,
563            #[serde(default)]
564            c: u32,
565        }
566
567        // New sender -> old receiver: the unknown `c` is ignored, not an error.
568        let mut new_bytes = Vec::new();
569        ciborium::into_writer(&New { a: 1, b: 2, c: 3 }, &mut new_bytes).unwrap();
570        let as_old: Old = ciborium::from_reader(&new_bytes[..]).unwrap();
571        assert_eq!((as_old.a, as_old.b), (1, 2));
572
573        // Old sender -> new receiver: the missing `c` falls back to its default.
574        let mut old_bytes = Vec::new();
575        ciborium::into_writer(&Old { a: 1, b: 2 }, &mut old_bytes).unwrap();
576        let as_new: New = ciborium::from_reader(&old_bytes[..]).unwrap();
577        assert_eq!(as_new, New { a: 1, b: 2, c: 0 });
578    }
579
580    #[test]
581    fn test_is_available_at() {
582        // Exec is in the generation-1 baseline: available to every peer.
583        assert!(MessageType::ExecRequest.is_available_at(1));
584        assert!(MessageType::ExecRequest.is_available_at(2));
585        assert!(MessageType::ExecRequest.is_available_at(PROTOCOL_VERSION));
586        // Filesystem requires generation 2: unavailable to a legacy (gen 1) peer.
587        assert!(!MessageType::FsRequest.is_available_at(1));
588        assert!(MessageType::FsRequest.is_available_at(2));
589        assert!(MessageType::FsRequest.is_available_at(PROTOCOL_VERSION));
590        // Ping/touch are generation-6 core capabilities.
591        assert!(!MessageType::Ping.is_available_at(5));
592        assert!(MessageType::Ping.is_available_at(6));
593        assert!(MessageType::Ping.is_available_at(PROTOCOL_VERSION));
594        // Bootstrap is internal to generation-7 host/agent boot.
595        assert!(!MessageType::Bootstrap.is_available_at(6));
596        assert!(MessageType::Bootstrap.is_available_at(PROTOCOL_VERSION));
597    }
598
599    #[test]
600    fn test_min_protocol_version_per_type() {
601        // Core and exec types are the generation-1 baseline: usable on every
602        // runtime we still talk to, including the pre-0.5 legacy one.
603        let baseline = [
604            MessageType::Ready,
605            MessageType::InitResolved,
606            MessageType::InitAck,
607            MessageType::Shutdown,
608            MessageType::RelayClientDisconnected,
609            MessageType::ClockSync,
610            MessageType::ExecRequest,
611            MessageType::ExecStarted,
612            MessageType::ExecStdin,
613            MessageType::ExecStdinError,
614            MessageType::ExecStdout,
615            MessageType::ExecStderr,
616            MessageType::ExecExited,
617            MessageType::ExecFailed,
618            MessageType::ExecResize,
619            MessageType::ExecSignal,
620        ];
621        for mt in &baseline {
622            assert_eq!(mt.min_protocol_version(), 1, "{mt:?} should be v1 baseline");
623        }
624
625        // Filesystem streaming did not exist in the pre-0.5 legacy protocol, so
626        // these require a post-legacy generation.
627        for mt in [
628            MessageType::FsRequest,
629            MessageType::FsResponse,
630            MessageType::FsData,
631        ] {
632            assert_eq!(mt.min_protocol_version(), 2, "{mt:?} should require gen 2");
633        }
634
635        for mt in [
636            MessageType::Ping,
637            MessageType::Pong,
638            MessageType::Touch,
639            MessageType::Touched,
640        ] {
641            assert_eq!(mt.min_protocol_version(), 6, "{mt:?} should require gen 6");
642        }
643
644        assert_eq!(MessageType::Bootstrap.min_protocol_version(), 7);
645
646        // Every current type must be sendable to a current peer.
647        assert!(MessageType::FsRequest.min_protocol_version() <= PROTOCOL_VERSION);
648    }
649
650    #[test]
651    fn test_message_new_computes_flags() {
652        let msg = Message::new(MessageType::ExecRequest, 1, Vec::new());
653        assert_eq!(msg.flags, FLAG_SESSION_START);
654
655        let msg = Message::new(MessageType::ExecStdout, 1, Vec::new());
656        assert_eq!(msg.flags, 0);
657    }
658}