Codex Handbook
windows-sandbox-rs/src/elevated/ipc_framed.rs 239 lines
//! Framed IPC protocol used between the parent (CLI) and the elevated command runner.//!//! This module defines the JSON message schema (spawn request/ready, output, stdin,//! exit, error, terminate) plus length‑prefixed framing helpers for a byte stream.//! It is **elevated-path only**: the parent uses it to bootstrap the runner and//! stream unified_exec I/O over named pipes. The legacy restricted‑token path does//! not use this protocol, and non‑unified exec capture uses it only when running//! through the elevated runner.use anyhow::Result;use base64::Engine as _;use base64::engine::general_purpose::STANDARD;use codex_protocol::models::PermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use serde::Deserialize;use serde::Serialize;use std::collections::HashMap;use std::io::Read;use std::io::Write;use std::path::PathBuf;/// Safety cap for a single framed message payload.////// This is not a protocol requirement; it simply bounds memory use and rejects/// obviously invalid frames.const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;/// Protocol version shared by the parent process and elevated command runner.pub const IPC_PROTOCOL_VERSION: u8 = 3;/// Length-prefixed, JSON-encoded frame.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct FramedMessage {    pub version: u8,    #[serde(flatten)]    pub message: Message,}/// IPC message variants exchanged between parent and runner.////// `SpawnRequest`, `Stdin`, `CloseStdin`, `Resize`, and `Terminate` are parent->runner commands./// `SpawnReady`, `Output`, `Exit`, and `Error` are runner->parent events/results.#[derive(Debug, Serialize, Deserialize, Clone)]#[serde(tag = "type", rename_all = "snake_case")]pub enum Message {    SpawnRequest { payload: Box<SpawnRequest> },    SpawnReady { payload: SpawnReady },    Output { payload: OutputPayload },    Stdin { payload: StdinPayload },    CloseStdin { payload: EmptyPayload },    Resize { payload: ResizePayload },    Exit { payload: ExitPayload },    Error { payload: ErrorPayload },    Terminate { payload: EmptyPayload },}/// Spawn parameters sent from parent to runner.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct SpawnRequest {    pub command: Vec<String>,    pub cwd: PathBuf,    pub env: HashMap<String, String>,    pub permission_profile: PermissionProfile,    pub workspace_roots: Vec<AbsolutePathBuf>,    pub codex_home: PathBuf,    pub real_codex_home: PathBuf,    pub cap_sids: Vec<String>,    pub timeout_ms: Option<u64>,    pub tty: bool,    #[serde(default)]    pub stdin_open: bool,    #[serde(default)]    pub use_private_desktop: bool,}/// Ack from runner after it spawns the child process.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct SpawnReady {    pub process_id: u32,}/// Output data sent from runner to parent.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct OutputPayload {    pub data_b64: String,    pub stream: OutputStream,}/// Output stream identifier for `OutputPayload`.#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]#[serde(rename_all = "snake_case")]pub enum OutputStream {    Stdout,    Stderr,}/// Stdin bytes sent from parent to runner.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct StdinPayload {    pub data_b64: String,}/// PTY resize request sent from parent to runner.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct ResizePayload {    pub rows: u16,    pub cols: u16,}/// Exit status sent from runner to parent.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct ExitPayload {    pub exit_code: i32,    pub timed_out: bool,}/// Error payload sent when the runner fails to spawn or stream.#[derive(Debug, Serialize, Deserialize, Clone)]pub struct ErrorPayload {    pub message: String,    pub code: String,}/// Empty payload for control messages.#[derive(Debug, Serialize, Deserialize, Clone, Default)]pub struct EmptyPayload {}/// Base64-encode raw bytes for IPC payloads.pub fn encode_bytes(data: &[u8]) -> String {    STANDARD.encode(data)}/// Decode base64 payload data into raw bytes.pub fn decode_bytes(data: &str) -> Result<Vec<u8>> {    Ok(STANDARD.decode(data.as_bytes())?)}/// Write a length-prefixed JSON frame.pub fn write_frame<W: Write>(mut writer: W, msg: &FramedMessage) -> Result<()> {    let payload = serde_json::to_vec(msg)?;    if payload.len() > MAX_FRAME_LEN {        anyhow::bail!("frame too large: {}", payload.len());    }    let len = payload.len() as u32;    writer.write_all(&len.to_le_bytes())?;    writer.write_all(&payload)?;    writer.flush()?;    Ok(())}/// Read a length-prefixed JSON frame; returns `Ok(None)` on EOF.pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {    let mut len_buf = [0u8; 4];    match reader.read_exact(&mut len_buf) {        Ok(()) => {}        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),        Err(err) => return Err(err.into()),    }    let len = u32::from_le_bytes(len_buf) as usize;    if len > MAX_FRAME_LEN {        anyhow::bail!("frame too large: {len}");    }    let mut payload = vec![0u8; len];    reader.read_exact(&mut payload)?;    let msg: FramedMessage = serde_json::from_slice(&payload)?;    Ok(Some(msg))}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    #[test]    fn framed_round_trip() {        let msg = FramedMessage {            version: IPC_PROTOCOL_VERSION,            message: Message::Output {                payload: OutputPayload {                    data_b64: encode_bytes(b"hello"),                    stream: OutputStream::Stdout,                },            },        };        let mut buf = Vec::new();        write_frame(&mut buf, &msg).expect("write");        let decoded = read_frame(buf.as_slice()).expect("read").expect("some");        assert_eq!(decoded.version, IPC_PROTOCOL_VERSION);        match decoded.message {            Message::Output { payload } => {                assert_eq!(payload.stream, OutputStream::Stdout);                let data = decode_bytes(&payload.data_b64).expect("decode");                assert_eq!(data, b"hello");            }            other => panic!("unexpected message: {other:?}"),        }    }    #[test]    fn spawn_request_serializes_permission_profile() {        let workspace_roots = vec![            AbsolutePathBuf::from_absolute_path(PathBuf::from(r"C:\workspace"))                .expect("absolute workspace root"),        ];        let msg = FramedMessage {            version: IPC_PROTOCOL_VERSION,            message: Message::SpawnRequest {                payload: Box::new(SpawnRequest {                    command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],                    cwd: PathBuf::from(r"C:\workspace"),                    env: HashMap::new(),                    permission_profile: PermissionProfile::read_only(),                    workspace_roots: workspace_roots.clone(),                    codex_home: PathBuf::from(r"C:\codex"),                    real_codex_home: PathBuf::from(r"C:\Users\codex"),                    cap_sids: vec!["S-1-15-3-1024-1".to_string()],                    timeout_ms: Some(1000),                    tty: false,                    stdin_open: false,                    use_private_desktop: false,                }),            },        };        let encoded = serde_json::to_value(&msg).expect("serialize");        assert_eq!("spawn_request", encoded["type"]);        assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);        assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));        assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));        assert_eq!(None, encoded["payload"].get("permission_profile_cwd"));        let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");        let Message::SpawnRequest { payload } = decoded.message else {            panic!("unexpected message");        };        assert_eq!(PermissionProfile::read_only(), payload.permission_profile);        assert_eq!(workspace_roots, payload.workspace_roots);    }}