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 = 3;
13
14/// Frame flag: this is the last message for the given correlation ID.
15///
16/// Set on terminal message types such as `ExecExited` and `FsResponse`.
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` and `FsRequest`.
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 version.
49    pub v: u8,
50
51    /// Message type.
52    pub t: MessageType,
53
54    /// Correlation ID used to associate requests with responses and
55    /// to identify exec sessions.
56    ///
57    /// Serialized in the binary frame header, not in CBOR.
58    #[serde(skip)]
59    pub id: u32,
60
61    /// Frame flags computed from the message type.
62    ///
63    /// Serialized in the binary frame header, not in CBOR.
64    #[serde(skip)]
65    pub flags: u8,
66
67    /// The CBOR-encoded payload bytes.
68    #[serde(with = "serde_bytes")]
69    pub p: Vec<u8>,
70}
71
72/// Identifies the type of a protocol message.
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub enum MessageType {
75    /// Guest agent is ready.
76    Ready,
77
78    /// Guest reports init context before user mounts.
79    InitResolved,
80
81    /// Host acknowledges init-context setup.
82    InitAck,
83
84    /// Host requests shutdown.
85    Shutdown,
86
87    /// Host relay reports that one SDK client disconnected.
88    RelayClientDisconnected,
89
90    /// Host asks the guest to synchronize `CLOCK_REALTIME`.
91    ClockSync,
92
93    /// Host requests command execution.
94    ExecRequest,
95
96    /// Guest confirms command started.
97    ExecStarted,
98
99    /// Host sends stdin data.
100    ExecStdin,
101
102    /// Guest reports that a prior `ExecStdin` write to the child's
103    /// stdin failed (e.g. the child closed its read end). Non-terminal:
104    /// the session continues and may still produce stdout/stderr and
105    /// an exit code.
106    ExecStdinError,
107
108    /// Guest sends stdout data.
109    ExecStdout,
110
111    /// Guest sends stderr data.
112    ExecStderr,
113
114    /// Guest reports command exit.
115    ExecExited,
116
117    /// Guest reports command failed to spawn (binary not found,
118    /// permission denied, etc.). Distinct from `ExecExited` —
119    /// `ExecFailed` means the user code never ran. Terminal.
120    ExecFailed,
121
122    /// Host requests PTY resize.
123    ExecResize,
124
125    /// Host sends signal to process.
126    ExecSignal,
127
128    /// Host requests a filesystem operation.
129    FsRequest,
130
131    /// Guest sends a terminal filesystem response.
132    FsResponse,
133
134    /// Streaming file data chunk (bidirectional).
135    FsData,
136}
137
138//--------------------------------------------------------------------------------------------------
139// Methods
140//--------------------------------------------------------------------------------------------------
141
142impl Message {
143    /// Creates a new message with the current protocol version and raw payload bytes.
144    pub fn new(t: MessageType, id: u32, p: Vec<u8>) -> Self {
145        let flags = t.flags();
146        Self {
147            v: PROTOCOL_VERSION,
148            t,
149            id,
150            flags,
151            p,
152        }
153    }
154
155    /// Creates a new message by serializing the given payload to CBOR.
156    pub fn with_payload<T: Serialize>(
157        t: MessageType,
158        id: u32,
159        payload: &T,
160    ) -> ProtocolResult<Self> {
161        let mut p = Vec::new();
162        ciborium::into_writer(payload, &mut p)?;
163        let flags = t.flags();
164        Ok(Self {
165            v: PROTOCOL_VERSION,
166            t,
167            id,
168            flags,
169            p,
170        })
171    }
172
173    /// Deserializes the payload bytes into the given type.
174    pub fn payload<T: DeserializeOwned>(&self) -> ProtocolResult<T> {
175        Ok(ciborium::from_reader(&self.p[..])?)
176    }
177}
178
179impl MessageType {
180    /// Computes the frame flags byte for this message type.
181    pub fn flags(&self) -> u8 {
182        match self {
183            Self::ExecExited | Self::ExecFailed | Self::FsResponse => FLAG_TERMINAL,
184            Self::ExecRequest | Self::FsRequest => FLAG_SESSION_START,
185            Self::Shutdown => FLAG_SHUTDOWN,
186            _ => 0,
187        }
188    }
189
190    /// Returns the wire string representation.
191    pub fn as_str(&self) -> &'static str {
192        match self {
193            Self::Ready => "core.ready",
194            Self::InitResolved => "core.init.resolved",
195            Self::InitAck => "core.init.ack",
196            Self::Shutdown => "core.shutdown",
197            Self::RelayClientDisconnected => "core.relay.client.disconnected",
198            Self::ClockSync => "core.clock.sync",
199            Self::ExecRequest => "core.exec.request",
200            Self::ExecStarted => "core.exec.started",
201            Self::ExecStdin => "core.exec.stdin",
202            Self::ExecStdinError => "core.exec.stdin.error",
203            Self::ExecStdout => "core.exec.stdout",
204            Self::ExecStderr => "core.exec.stderr",
205            Self::ExecExited => "core.exec.exited",
206            Self::ExecFailed => "core.exec.failed",
207            Self::ExecResize => "core.exec.resize",
208            Self::ExecSignal => "core.exec.signal",
209            Self::FsRequest => "core.fs.request",
210            Self::FsResponse => "core.fs.response",
211            Self::FsData => "core.fs.data",
212        }
213    }
214
215    /// Parses a wire string into a message type.
216    pub fn from_wire_str(s: &str) -> Option<Self> {
217        match s {
218            "core.ready" => Some(Self::Ready),
219            "core.init.resolved" => Some(Self::InitResolved),
220            "core.init.ack" => Some(Self::InitAck),
221            "core.shutdown" => Some(Self::Shutdown),
222            "core.relay.client.disconnected" => Some(Self::RelayClientDisconnected),
223            "core.clock.sync" => Some(Self::ClockSync),
224            "core.exec.request" => Some(Self::ExecRequest),
225            "core.exec.started" => Some(Self::ExecStarted),
226            "core.exec.stdin" => Some(Self::ExecStdin),
227            "core.exec.stdin.error" => Some(Self::ExecStdinError),
228            "core.exec.stdout" => Some(Self::ExecStdout),
229            "core.exec.stderr" => Some(Self::ExecStderr),
230            "core.exec.exited" => Some(Self::ExecExited),
231            "core.exec.failed" => Some(Self::ExecFailed),
232            "core.exec.resize" => Some(Self::ExecResize),
233            "core.exec.signal" => Some(Self::ExecSignal),
234            "core.fs.request" => Some(Self::FsRequest),
235            "core.fs.response" => Some(Self::FsResponse),
236            "core.fs.data" => Some(Self::FsData),
237            _ => None,
238        }
239    }
240}
241
242//--------------------------------------------------------------------------------------------------
243// Trait Implementations
244//--------------------------------------------------------------------------------------------------
245
246impl Serialize for MessageType {
247    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248    where
249        S: serde::Serializer,
250    {
251        serializer.serialize_str(self.as_str())
252    }
253}
254
255impl<'de> Deserialize<'de> for MessageType {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: serde::Deserializer<'de>,
259    {
260        let s = String::deserialize(deserializer)?;
261        Self::from_wire_str(&s)
262            .ok_or_else(|| serde::de::Error::custom(format!("unknown message type: {s}")))
263    }
264}
265
266//--------------------------------------------------------------------------------------------------
267// Tests
268//--------------------------------------------------------------------------------------------------
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_message_type_roundtrip() {
276        let types = [
277            (MessageType::Ready, "core.ready"),
278            (MessageType::InitResolved, "core.init.resolved"),
279            (MessageType::InitAck, "core.init.ack"),
280            (MessageType::Shutdown, "core.shutdown"),
281            (
282                MessageType::RelayClientDisconnected,
283                "core.relay.client.disconnected",
284            ),
285            (MessageType::ClockSync, "core.clock.sync"),
286            (MessageType::ExecRequest, "core.exec.request"),
287            (MessageType::ExecStarted, "core.exec.started"),
288            (MessageType::ExecStdin, "core.exec.stdin"),
289            (MessageType::ExecStdinError, "core.exec.stdin.error"),
290            (MessageType::ExecStdout, "core.exec.stdout"),
291            (MessageType::ExecStderr, "core.exec.stderr"),
292            (MessageType::ExecExited, "core.exec.exited"),
293            (MessageType::ExecFailed, "core.exec.failed"),
294            (MessageType::ExecResize, "core.exec.resize"),
295            (MessageType::ExecSignal, "core.exec.signal"),
296            (MessageType::FsRequest, "core.fs.request"),
297            (MessageType::FsResponse, "core.fs.response"),
298            (MessageType::FsData, "core.fs.data"),
299        ];
300
301        for (mt, expected_str) in &types {
302            assert_eq!(mt.as_str(), *expected_str);
303            assert_eq!(MessageType::from_wire_str(expected_str).unwrap(), *mt);
304        }
305    }
306
307    #[test]
308    fn test_message_type_serde_roundtrip() {
309        let types = [
310            MessageType::Ready,
311            MessageType::InitResolved,
312            MessageType::InitAck,
313            MessageType::Shutdown,
314            MessageType::RelayClientDisconnected,
315            MessageType::ClockSync,
316            MessageType::ExecRequest,
317            MessageType::ExecStarted,
318            MessageType::ExecStdin,
319            MessageType::ExecStdinError,
320            MessageType::ExecStdout,
321            MessageType::ExecStderr,
322            MessageType::ExecExited,
323            MessageType::ExecFailed,
324            MessageType::ExecResize,
325            MessageType::ExecSignal,
326            MessageType::FsRequest,
327            MessageType::FsResponse,
328            MessageType::FsData,
329        ];
330
331        for mt in &types {
332            let mut buf = Vec::new();
333            ciborium::into_writer(mt, &mut buf).unwrap();
334            let decoded: MessageType = ciborium::from_reader(&buf[..]).unwrap();
335            assert_eq!(&decoded, mt);
336        }
337    }
338
339    #[test]
340    fn test_unknown_message_type() {
341        assert!(MessageType::from_wire_str("core.unknown").is_none());
342    }
343
344    #[test]
345    fn test_message_with_payload_roundtrip() {
346        use crate::exec::ExecExited;
347
348        let msg =
349            Message::with_payload(MessageType::ExecExited, 7, &ExecExited { code: 42 }).unwrap();
350
351        assert_eq!(msg.t, MessageType::ExecExited);
352        assert_eq!(msg.id, 7);
353        assert_eq!(msg.flags, FLAG_TERMINAL);
354
355        let payload: ExecExited = msg.payload().unwrap();
356        assert_eq!(payload.code, 42);
357    }
358
359    #[test]
360    fn test_message_type_flags() {
361        assert_eq!(MessageType::ExecExited.flags(), FLAG_TERMINAL);
362        assert_eq!(MessageType::ExecFailed.flags(), FLAG_TERMINAL);
363        assert_eq!(MessageType::FsResponse.flags(), FLAG_TERMINAL);
364        assert_eq!(MessageType::ExecRequest.flags(), FLAG_SESSION_START);
365        assert_eq!(MessageType::FsRequest.flags(), FLAG_SESSION_START);
366        assert_eq!(MessageType::Ready.flags(), 0);
367        assert_eq!(MessageType::InitResolved.flags(), 0);
368        assert_eq!(MessageType::InitAck.flags(), 0);
369        assert_eq!(MessageType::Shutdown.flags(), FLAG_SHUTDOWN);
370        assert_eq!(MessageType::ClockSync.flags(), 0);
371        assert_eq!(MessageType::ExecStarted.flags(), 0);
372        assert_eq!(MessageType::ExecStdin.flags(), 0);
373        assert_eq!(MessageType::ExecStdout.flags(), 0);
374        assert_eq!(MessageType::ExecStderr.flags(), 0);
375        assert_eq!(MessageType::ExecResize.flags(), 0);
376        assert_eq!(MessageType::ExecSignal.flags(), 0);
377        assert_eq!(MessageType::FsData.flags(), 0);
378    }
379
380    #[test]
381    fn test_message_new_computes_flags() {
382        let msg = Message::new(MessageType::ExecRequest, 1, Vec::new());
383        assert_eq!(msg.flags, FLAG_SESSION_START);
384
385        let msg = Message::new(MessageType::ExecStdout, 1, Vec::new());
386        assert_eq!(msg.flags, 0);
387    }
388}