Skip to main content

agentos_execution/
v8_ipc.rs

1//! Binary IPC framing for communication with the secure-exec-v8 runtime process.
2//!
3//! Wire format per frame:
4//!   [4B total_len (u32 BE, excludes self)]
5//!   [1B msg_type]
6//!   [1B sid_len (N)]
7//!   [N bytes session_id (UTF-8)]
8//!   [... type-specific fixed fields ...]
9//!   [M bytes payload (rest of frame)]
10
11use std::io;
12
13/// Maximum frame payload: 64 MB.
14const MAX_FRAME_SIZE: u32 = 64 * 1024 * 1024;
15
16// Host → V8 message type codes
17const MSG_AUTHENTICATE: u8 = 0x01;
18const MSG_CREATE_SESSION: u8 = 0x02;
19const MSG_DESTROY_SESSION: u8 = 0x03;
20const MSG_INJECT_GLOBALS: u8 = 0x04;
21const MSG_EXECUTE: u8 = 0x05;
22const MSG_BRIDGE_RESPONSE: u8 = 0x06;
23const MSG_STREAM_EVENT: u8 = 0x07;
24const MSG_TERMINATE_EXECUTION: u8 = 0x08;
25
26// V8 → Host message type codes
27const MSG_BRIDGE_CALL: u8 = 0x81;
28const MSG_EXECUTION_RESULT: u8 = 0x82;
29const MSG_LOG: u8 = 0x83;
30const MSG_STREAM_CALLBACK: u8 = 0x84;
31
32// ExecutionResult flags
33const FLAG_HAS_EXPORTS: u8 = 0x01;
34const FLAG_HAS_ERROR: u8 = 0x02;
35
36/// A decoded binary frame.
37#[derive(Debug, Clone, PartialEq)]
38pub enum BinaryFrame {
39    // Host → V8
40    Authenticate {
41        token: String,
42    },
43    CreateSession {
44        session_id: String,
45        heap_limit_mb: u32,
46        cpu_time_limit_ms: u32,
47        wall_clock_limit_ms: u32,
48    },
49    DestroySession {
50        session_id: String,
51    },
52    InjectGlobals {
53        session_id: String,
54        payload: Vec<u8>,
55    },
56    Execute {
57        session_id: String,
58        mode: u8, // 0 = exec (CJS), 1 = run (ESM)
59        file_path: String,
60        bridge_code: String,
61        post_restore_script: String,
62        // Optional agent-SDK bundle evaluated into the per-sidecar snapshot
63        // alongside the bridge (empty = bridge-only snapshot). Must stay
64        // wire-compatible with v8-runtime's ipc_binary BinaryFrame::Execute.
65        userland_code: String,
66        high_resolution_time: bool,
67        user_code: String,
68    },
69    BridgeResponse {
70        session_id: String,
71        call_id: u64,
72        status: u8, // 0 = success, 1 = error, 2 = raw binary
73        payload: Vec<u8>,
74    },
75    StreamEvent {
76        session_id: String,
77        event_type: String,
78        payload: Vec<u8>,
79    },
80    TerminateExecution {
81        session_id: String,
82    },
83
84    // V8 → Host
85    BridgeCall {
86        session_id: String,
87        call_id: u64,
88        method: String,
89        payload: Vec<u8>,
90    },
91    ExecutionResult {
92        session_id: String,
93        exit_code: i32,
94        exports: Option<Vec<u8>>,
95        error: Option<ExecutionErrorBin>,
96    },
97    Log {
98        session_id: String,
99        channel: u8, // 0 = stdout, 1 = stderr
100        message: String,
101    },
102    StreamCallback {
103        session_id: String,
104        callback_type: String,
105        payload: Vec<u8>,
106    },
107}
108
109/// Structured error from V8 execution.
110#[derive(Debug, Clone, PartialEq)]
111pub struct ExecutionErrorBin {
112    pub error_type: String,
113    pub message: String,
114    pub stack: String,
115    pub code: String,
116}
117
118/// Encode a frame into a byte buffer (length prefix + body).
119pub fn encode_frame(frame: &BinaryFrame) -> io::Result<Vec<u8>> {
120    let mut buf = Vec::new();
121    // Reserve 4 bytes for length prefix
122    buf.extend_from_slice(&[0, 0, 0, 0]);
123    encode_body(&mut buf, frame)?;
124    let total_len = buf.len() - 4;
125    if total_len > MAX_FRAME_SIZE as usize {
126        return Err(io::Error::new(
127            io::ErrorKind::InvalidData,
128            format!("frame size {total_len} exceeds maximum {MAX_FRAME_SIZE}"),
129        ));
130    }
131    buf[..4].copy_from_slice(&(total_len as u32).to_be_bytes());
132    Ok(buf)
133}
134
135/// Decode a frame from raw bytes (after the 4-byte length prefix has been read).
136pub fn decode_frame(buf: &[u8]) -> io::Result<BinaryFrame> {
137    if buf.is_empty() {
138        return Err(io::Error::new(io::ErrorKind::InvalidData, "empty frame"));
139    }
140    if buf.len() > MAX_FRAME_SIZE as usize {
141        return Err(io::Error::new(
142            io::ErrorKind::InvalidData,
143            format!("frame size {} exceeds maximum {MAX_FRAME_SIZE}", buf.len()),
144        ));
145    }
146
147    let msg_type = buf[0];
148    let mut pos = 1;
149
150    let sid_len = read_u8(buf, &mut pos)? as usize;
151    let session_id = read_utf8(buf, &mut pos, sid_len)?;
152
153    match msg_type {
154        MSG_AUTHENTICATE => {
155            let remaining = buf.len() - pos;
156            let token = read_utf8(buf, &mut pos, remaining)?;
157            Ok(BinaryFrame::Authenticate { token })
158        }
159        MSG_CREATE_SESSION => {
160            let heap_limit_mb = read_u32(buf, &mut pos)?;
161            let cpu_time_limit_ms = read_u32(buf, &mut pos)?;
162            let wall_clock_limit_ms = read_u32(buf, &mut pos)?;
163            Ok(BinaryFrame::CreateSession {
164                session_id,
165                heap_limit_mb,
166                cpu_time_limit_ms,
167                wall_clock_limit_ms,
168            })
169        }
170        MSG_DESTROY_SESSION => Ok(BinaryFrame::DestroySession { session_id }),
171        MSG_INJECT_GLOBALS => {
172            let payload = buf[pos..].to_vec();
173            Ok(BinaryFrame::InjectGlobals {
174                session_id,
175                payload,
176            })
177        }
178        MSG_EXECUTE => {
179            let mode = read_u8(buf, &mut pos)?;
180            let fp_len = read_u16(buf, &mut pos)? as usize;
181            let file_path = read_utf8(buf, &mut pos, fp_len)?;
182            let bc_len = read_u32(buf, &mut pos)? as usize;
183            let bridge_code = read_utf8(buf, &mut pos, bc_len)?;
184            let prs_len = read_u32(buf, &mut pos)? as usize;
185            let post_restore_script = read_utf8(buf, &mut pos, prs_len)?;
186            let ul_len = read_u32(buf, &mut pos)? as usize;
187            let userland_code = read_utf8(buf, &mut pos, ul_len)?;
188            let high_resolution_time = read_u8(buf, &mut pos)? != 0;
189            let remaining = buf.len() - pos;
190            let user_code = read_utf8(buf, &mut pos, remaining)?;
191            Ok(BinaryFrame::Execute {
192                session_id,
193                mode,
194                file_path,
195                bridge_code,
196                post_restore_script,
197                userland_code,
198                high_resolution_time,
199                user_code,
200            })
201        }
202        MSG_BRIDGE_RESPONSE => {
203            let call_id = read_u64(buf, &mut pos)?;
204            let status = read_u8(buf, &mut pos)?;
205            let payload = buf[pos..].to_vec();
206            Ok(BinaryFrame::BridgeResponse {
207                session_id,
208                call_id,
209                status,
210                payload,
211            })
212        }
213        MSG_STREAM_EVENT => {
214            let et_len = read_u16(buf, &mut pos)? as usize;
215            let event_type = read_utf8(buf, &mut pos, et_len)?;
216            let payload = buf[pos..].to_vec();
217            Ok(BinaryFrame::StreamEvent {
218                session_id,
219                event_type,
220                payload,
221            })
222        }
223        MSG_TERMINATE_EXECUTION => Ok(BinaryFrame::TerminateExecution { session_id }),
224        MSG_BRIDGE_CALL => {
225            let call_id = read_u64(buf, &mut pos)?;
226            let m_len = read_u16(buf, &mut pos)? as usize;
227            let method = read_utf8(buf, &mut pos, m_len)?;
228            let payload = buf[pos..].to_vec();
229            Ok(BinaryFrame::BridgeCall {
230                session_id,
231                call_id,
232                method,
233                payload,
234            })
235        }
236        MSG_EXECUTION_RESULT => {
237            let exit_code = read_i32(buf, &mut pos)?;
238            let flags = read_u8(buf, &mut pos)?;
239            let exports = if flags & FLAG_HAS_EXPORTS != 0 {
240                let exp_len = read_u32(buf, &mut pos)? as usize;
241                let data = read_bytes(buf, &mut pos, exp_len)?;
242                Some(data)
243            } else {
244                None
245            };
246            let error = if flags & FLAG_HAS_ERROR != 0 {
247                let error_type = read_len_prefixed_u16(buf, &mut pos)?;
248                let message = read_len_prefixed_u16(buf, &mut pos)?;
249                let stack = read_len_prefixed_u16(buf, &mut pos)?;
250                let code = read_len_prefixed_u16(buf, &mut pos)?;
251                Some(ExecutionErrorBin {
252                    error_type,
253                    message,
254                    stack,
255                    code,
256                })
257            } else {
258                None
259            };
260            Ok(BinaryFrame::ExecutionResult {
261                session_id,
262                exit_code,
263                exports,
264                error,
265            })
266        }
267        MSG_LOG => {
268            let channel = read_u8(buf, &mut pos)?;
269            let remaining = buf.len() - pos;
270            let message = read_utf8(buf, &mut pos, remaining)?;
271            Ok(BinaryFrame::Log {
272                session_id,
273                channel,
274                message,
275            })
276        }
277        MSG_STREAM_CALLBACK => {
278            let ct_len = read_u16(buf, &mut pos)? as usize;
279            let callback_type = read_utf8(buf, &mut pos, ct_len)?;
280            let payload = buf[pos..].to_vec();
281            Ok(BinaryFrame::StreamCallback {
282                session_id,
283                callback_type,
284                payload,
285            })
286        }
287        _ => Err(io::Error::new(
288            io::ErrorKind::InvalidData,
289            format!("unknown message type: 0x{msg_type:02x}"),
290        )),
291    }
292}
293
294// -- Encode body --
295
296fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
297    match frame {
298        BinaryFrame::Authenticate { token } => {
299            buf.push(MSG_AUTHENTICATE);
300            buf.push(0); // no session_id
301            buf.extend_from_slice(token.as_bytes());
302        }
303        BinaryFrame::CreateSession {
304            session_id,
305            heap_limit_mb,
306            cpu_time_limit_ms,
307            wall_clock_limit_ms,
308        } => {
309            buf.push(MSG_CREATE_SESSION);
310            write_session_id(buf, session_id)?;
311            buf.extend_from_slice(&heap_limit_mb.to_be_bytes());
312            buf.extend_from_slice(&cpu_time_limit_ms.to_be_bytes());
313            buf.extend_from_slice(&wall_clock_limit_ms.to_be_bytes());
314        }
315        BinaryFrame::DestroySession { session_id } => {
316            buf.push(MSG_DESTROY_SESSION);
317            write_session_id(buf, session_id)?;
318        }
319        BinaryFrame::InjectGlobals {
320            session_id,
321            payload,
322        } => {
323            buf.push(MSG_INJECT_GLOBALS);
324            write_session_id(buf, session_id)?;
325            buf.extend_from_slice(payload);
326        }
327        BinaryFrame::Execute {
328            session_id,
329            mode,
330            file_path,
331            bridge_code,
332            post_restore_script,
333            userland_code,
334            high_resolution_time,
335            user_code,
336        } => {
337            buf.push(MSG_EXECUTE);
338            write_session_id(buf, session_id)?;
339            buf.push(*mode);
340            write_len_prefixed_u16(buf, file_path)?;
341            let bc_bytes = bridge_code.as_bytes();
342            buf.extend_from_slice(&(bc_bytes.len() as u32).to_be_bytes());
343            buf.extend_from_slice(bc_bytes);
344            let prs_bytes = post_restore_script.as_bytes();
345            buf.extend_from_slice(&(prs_bytes.len() as u32).to_be_bytes());
346            buf.extend_from_slice(prs_bytes);
347            let ul_bytes = userland_code.as_bytes();
348            buf.extend_from_slice(&(ul_bytes.len() as u32).to_be_bytes());
349            buf.extend_from_slice(ul_bytes);
350            buf.push(u8::from(*high_resolution_time));
351            buf.extend_from_slice(user_code.as_bytes());
352        }
353        BinaryFrame::BridgeResponse {
354            session_id,
355            call_id,
356            status,
357            payload,
358        } => {
359            buf.push(MSG_BRIDGE_RESPONSE);
360            write_session_id(buf, session_id)?;
361            buf.extend_from_slice(&call_id.to_be_bytes());
362            buf.push(*status);
363            buf.extend_from_slice(payload);
364        }
365        BinaryFrame::StreamEvent {
366            session_id,
367            event_type,
368            payload,
369        } => {
370            buf.push(MSG_STREAM_EVENT);
371            write_session_id(buf, session_id)?;
372            write_len_prefixed_u16(buf, event_type)?;
373            buf.extend_from_slice(payload);
374        }
375        BinaryFrame::TerminateExecution { session_id } => {
376            buf.push(MSG_TERMINATE_EXECUTION);
377            write_session_id(buf, session_id)?;
378        }
379        // V8→Host frames: include encoding for completeness/testing
380        BinaryFrame::BridgeCall {
381            session_id,
382            call_id,
383            method,
384            payload,
385        } => {
386            buf.push(MSG_BRIDGE_CALL);
387            write_session_id(buf, session_id)?;
388            buf.extend_from_slice(&call_id.to_be_bytes());
389            write_len_prefixed_u16(buf, method)?;
390            buf.extend_from_slice(payload);
391        }
392        BinaryFrame::ExecutionResult {
393            session_id,
394            exit_code,
395            exports,
396            error,
397        } => {
398            buf.push(MSG_EXECUTION_RESULT);
399            write_session_id(buf, session_id)?;
400            buf.extend_from_slice(&exit_code.to_be_bytes());
401            let mut flags: u8 = 0;
402            if exports.is_some() {
403                flags |= FLAG_HAS_EXPORTS;
404            }
405            if error.is_some() {
406                flags |= FLAG_HAS_ERROR;
407            }
408            buf.push(flags);
409            if let Some(exp) = exports {
410                buf.extend_from_slice(&(exp.len() as u32).to_be_bytes());
411                buf.extend_from_slice(exp);
412            }
413            if let Some(err) = error {
414                write_len_prefixed_u16(buf, &err.error_type)?;
415                write_len_prefixed_u16(buf, &err.message)?;
416                write_len_prefixed_u16(buf, &err.stack)?;
417                write_len_prefixed_u16(buf, &err.code)?;
418            }
419        }
420        BinaryFrame::Log {
421            session_id,
422            channel,
423            message,
424        } => {
425            buf.push(MSG_LOG);
426            write_session_id(buf, session_id)?;
427            buf.push(*channel);
428            buf.extend_from_slice(message.as_bytes());
429        }
430        BinaryFrame::StreamCallback {
431            session_id,
432            callback_type,
433            payload,
434        } => {
435            buf.push(MSG_STREAM_CALLBACK);
436            write_session_id(buf, session_id)?;
437            write_len_prefixed_u16(buf, callback_type)?;
438            buf.extend_from_slice(payload);
439        }
440    }
441    Ok(())
442}
443
444// -- Primitive helpers --
445
446fn write_session_id(buf: &mut Vec<u8>, sid: &str) -> io::Result<()> {
447    let bytes = sid.as_bytes();
448    if bytes.len() > 255 {
449        return Err(io::Error::new(
450            io::ErrorKind::InvalidInput,
451            format!("session ID byte length {} exceeds u8 max 255", bytes.len()),
452        ));
453    }
454    buf.push(bytes.len() as u8);
455    buf.extend_from_slice(bytes);
456    Ok(())
457}
458
459fn write_len_prefixed_u16(buf: &mut Vec<u8>, s: &str) -> io::Result<()> {
460    let bytes = s.as_bytes();
461    if bytes.len() > 0xFFFF {
462        return Err(io::Error::new(
463            io::ErrorKind::InvalidInput,
464            format!("string byte length {} exceeds u16 max 65535", bytes.len()),
465        ));
466    }
467    buf.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
468    buf.extend_from_slice(bytes);
469    Ok(())
470}
471
472fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
473    if *pos >= buf.len() {
474        return Err(eof());
475    }
476    let v = buf[*pos];
477    *pos += 1;
478    Ok(v)
479}
480
481fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
482    if *pos + 2 > buf.len() {
483        return Err(eof());
484    }
485    let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
486    *pos += 2;
487    Ok(v)
488}
489
490fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
491    if *pos + 4 > buf.len() {
492        return Err(eof());
493    }
494    let v = u32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
495    *pos += 4;
496    Ok(v)
497}
498
499fn read_i32(buf: &[u8], pos: &mut usize) -> io::Result<i32> {
500    if *pos + 4 > buf.len() {
501        return Err(eof());
502    }
503    let v = i32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
504    *pos += 4;
505    Ok(v)
506}
507
508fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
509    if *pos + 8 > buf.len() {
510        return Err(eof());
511    }
512    let mut bytes = [0u8; 8];
513    bytes.copy_from_slice(&buf[*pos..*pos + 8]);
514    *pos += 8;
515    Ok(u64::from_be_bytes(bytes))
516}
517
518fn read_utf8(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
519    if *pos + len > buf.len() {
520        return Err(eof());
521    }
522    let s = std::str::from_utf8(&buf[*pos..*pos + len])
523        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
524    *pos += len;
525    Ok(s.to_owned())
526}
527
528fn read_bytes(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<Vec<u8>> {
529    if *pos + len > buf.len() {
530        return Err(eof());
531    }
532    let data = buf[*pos..*pos + len].to_vec();
533    *pos += len;
534    Ok(data)
535}
536
537fn read_len_prefixed_u16(buf: &[u8], pos: &mut usize) -> io::Result<String> {
538    let len = read_u16(buf, pos)? as usize;
539    read_utf8(buf, pos, len)
540}
541
542fn eof() -> io::Error {
543    io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of frame")
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn roundtrip_authenticate() {
552        let frame = BinaryFrame::Authenticate {
553            token: "secret123".into(),
554        };
555        let bytes = encode_frame(&frame).unwrap();
556        let decoded = decode_frame(&bytes[4..]).unwrap();
557        assert_eq!(frame, decoded);
558    }
559
560    #[test]
561    fn roundtrip_create_session() {
562        let frame = BinaryFrame::CreateSession {
563            session_id: "sess-1".into(),
564            heap_limit_mb: 256,
565            cpu_time_limit_ms: 30000,
566            wall_clock_limit_ms: 45000,
567        };
568        let bytes = encode_frame(&frame).unwrap();
569        let decoded = decode_frame(&bytes[4..]).unwrap();
570        assert_eq!(frame, decoded);
571    }
572
573    #[test]
574    fn roundtrip_bridge_call() {
575        let frame = BinaryFrame::BridgeCall {
576            session_id: "sess-1".into(),
577            call_id: 42,
578            method: "_fsReadFile".into(),
579            payload: vec![1, 2, 3],
580        };
581        let bytes = encode_frame(&frame).unwrap();
582        let decoded = decode_frame(&bytes[4..]).unwrap();
583        assert_eq!(frame, decoded);
584    }
585
586    #[test]
587    fn roundtrip_execution_result_with_error() {
588        let frame = BinaryFrame::ExecutionResult {
589            session_id: "sess-1".into(),
590            exit_code: 1,
591            exports: None,
592            error: Some(ExecutionErrorBin {
593                error_type: "Error".into(),
594                message: "something failed".into(),
595                stack: "at foo:1:1".into(),
596                code: "ERR_TEST".into(),
597            }),
598        };
599        let bytes = encode_frame(&frame).unwrap();
600        let decoded = decode_frame(&bytes[4..]).unwrap();
601        assert_eq!(frame, decoded);
602    }
603
604    #[test]
605    fn decode_frame_rejects_oversized_body() {
606        let oversized = vec![0u8; MAX_FRAME_SIZE as usize + 1];
607        let result = decode_frame(&oversized);
608        assert!(result.is_err());
609        let err = result.unwrap_err();
610        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
611        assert!(err.to_string().contains("exceeds maximum"));
612    }
613}