Skip to main content

agentos_execution/
v8_ipc.rs

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