Codex Handbook
tui/src/ide_context/ipc.rs 1009 lines
//! Private transport for fetching IDE context for TUI `/ide` support.use std::path::Path;use std::path::PathBuf;use std::time::Duration;use std::time::Instant;#[cfg(any(unix, windows))]use serde_json::Value;#[cfg(any(unix, windows, test))]use serde_json::json;use thiserror::Error;use super::IdeContext;// The desktop IPC client gives requests 5 seconds to complete. Match that prompt-time budget here:// fetching IDE context includes router discovery and extension event-loop work, so a shorter TUI// deadline can incorrectly skip context even though the IDE answers normally.const IDE_CONTEXT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);#[cfg(any(unix, windows))]const MAX_IPC_FRAME_BYTES: usize = 256 * 1024 * 1024;#[cfg(any(unix, windows))]const TUI_SOURCE_CLIENT_ID: &str = "codex-tui";#[cfg(any(unix, windows))]const OPEN_IDE_HINT: &str =    "Open this project in VS Code or Cursor with the Codex extension active.";#[cfg(any(unix, windows))]const IDE_DID_NOT_PROVIDE_CONTEXT_HINT: &str = "The IDE extension did not provide context.";#[cfg(any(unix, windows))]const KEEP_TRYING_HINT: &str = "Codex will keep trying on future messages.";#[derive(Debug, Error)]pub(crate) enum IdeContextError {    #[cfg(any(unix, windows))]    #[error("failed to connect to IDE context provider: {0}")]    Connect(std::io::Error),    #[cfg(any(unix, windows))]    #[error("failed to request IDE context: {0}")]    Send(std::io::Error),    #[cfg(any(unix, windows))]    #[error("failed to read IDE context: {0}")]    Read(std::io::Error),    #[cfg(any(unix, windows))]    #[error("invalid IDE context response: {0}")]    InvalidResponse(String),    #[cfg(any(unix, windows))]    #[error("IDE context response exceeded maximum size")]    ResponseTooLarge,    #[cfg(any(unix, windows))]    #[error("IDE context request failed")]    RequestFailed(String),    #[cfg(not(any(unix, windows)))]    #[error("IDE context is not supported on this platform")]    UnsupportedPlatform,}impl IdeContextError {    #[cfg(any(unix, windows))]    pub(crate) fn user_facing_hint(&self) -> String {        match self {            IdeContextError::Connect(_) => OPEN_IDE_HINT.to_string(),            IdeContextError::RequestFailed(error) if error == "no-client-found" => {                OPEN_IDE_HINT.to_string()            }            IdeContextError::RequestFailed(_) => {                format!("{IDE_DID_NOT_PROVIDE_CONTEXT_HINT} Try /ide again.")            }            IdeContextError::ResponseTooLarge => {                "The selected IDE context is too large. Clear any large selection in your IDE and try /ide again.".to_string()            }            IdeContextError::Send(_) => {                "Codex could not request IDE context. Try /ide again.".to_string()            }            IdeContextError::Read(_) | IdeContextError::InvalidResponse(_) => {                "Codex could not read IDE context. Try /ide again.".to_string()            }        }    }    #[cfg(any(unix, windows))]    pub(crate) fn prompt_skip_hint(&self) -> String {        match self {            IdeContextError::ResponseTooLarge => {                "The selected IDE context is too large. Clear any large selection in your IDE."                    .to_string()            }            IdeContextError::Connect(_) => OPEN_IDE_HINT.to_string(),            IdeContextError::RequestFailed(error) if error == "no-client-found" => {                OPEN_IDE_HINT.to_string()            }            IdeContextError::Read(error) if error.kind() == std::io::ErrorKind::TimedOut => {                "Codex timed out waiting for IDE context. It will keep trying on future messages."                    .to_string()            }            IdeContextError::RequestFailed(error) if error == "client-disconnected" => {                hint_with_retry("The IDE connection changed while Codex was requesting context.")            }            IdeContextError::RequestFailed(error) if error == "request-timeout" => {                hint_with_retry("The IDE extension did not answer in time.")            }            IdeContextError::RequestFailed(error) if error == "request-version-mismatch" => {                "The connected IDE extension is not compatible with this IDE context request."                    .to_string()            }            IdeContextError::RequestFailed(error) if error == "no-handler-for-request" => {                "The connected IDE client does not support IDE context requests.".to_string()            }            IdeContextError::Send(_) => {                hint_with_retry("Codex lost the IDE connection while requesting context.")            }            IdeContextError::InvalidResponse(_) => {                hint_with_retry("Codex received an unexpected IDE context response.")            }            IdeContextError::RequestFailed(_) => hint_with_retry(IDE_DID_NOT_PROVIDE_CONTEXT_HINT),            IdeContextError::Read(_) => hint_with_retry("Codex could not read IDE context."),        }    }    #[cfg(not(any(unix, windows)))]    pub(crate) fn user_facing_hint(&self) -> String {        self.to_string()    }    #[cfg(not(any(unix, windows)))]    pub(crate) fn prompt_skip_hint(&self) -> String {        self.to_string()    }}#[cfg(any(unix, windows))]fn hint_with_retry(message: &str) -> String {    format!("{message} {KEEP_TRYING_HINT}")}#[cfg(unix)]type IdeContextStream = UnixDeadlineStream;#[cfg(windows)]type IdeContextStream = super::windows_pipe::WindowsPipeStream;#[cfg(any(unix, windows))]pub(crate) fn fetch_ide_context(workspace_root: &Path) -> Result<IdeContext, IdeContextError> {    fetch_ide_context_from_socket(        default_ipc_socket_path(),        workspace_root,        IDE_CONTEXT_REQUEST_TIMEOUT,    )}#[cfg(not(any(unix, windows)))]pub(crate) fn fetch_ide_context(_workspace_root: &Path) -> Result<IdeContext, IdeContextError> {    Err(IdeContextError::UnsupportedPlatform)}#[cfg(unix)]fn default_ipc_socket_path() -> PathBuf {    let uid = unsafe { libc::getuid() };    std::env::temp_dir()        .join("codex-ipc")        .join(format!("ipc-{uid}.sock"))}#[cfg(windows)]fn default_ipc_socket_path() -> PathBuf {    PathBuf::from(r"\\.\pipe\codex-ipc")}#[cfg(not(any(unix, windows)))]fn default_ipc_socket_path() -> PathBuf {    PathBuf::new()}#[cfg(any(unix, windows))]fn fetch_ide_context_from_socket(    socket_path: PathBuf,    workspace_root: &Path,    timeout: Duration,) -> Result<IdeContext, IdeContextError> {    let deadline = Instant::now() + timeout;    let mut stream = connect_stream(socket_path, deadline)?;    fetch_ide_context_from_stream(&mut stream, workspace_root, deadline)}#[cfg(unix)]fn connect_stream(    socket_path: PathBuf,    deadline: Instant,) -> Result<IdeContextStream, IdeContextError> {    UnixDeadlineStream::connect(socket_path, deadline).map_err(IdeContextError::Connect)}#[cfg(unix)]struct UnixDeadlineStream {    stream: std::os::unix::net::UnixStream,    deadline: Instant,}#[cfg(unix)]impl UnixDeadlineStream {    fn connect(socket_path: PathBuf, deadline: Instant) -> std::io::Result<Self> {        let stream = connect_unix_stream_before_deadline(&socket_path, deadline)?;        validate_unix_peer_owner(&stream)?;        Ok(Self::new(stream, deadline))    }    fn new(stream: std::os::unix::net::UnixStream, deadline: Instant) -> Self {        Self { stream, deadline }    }    fn set_deadline(&mut self, deadline: Instant) {        self.deadline = deadline;    }    fn wait_for_ready(&self, events: libc::c_short) -> std::io::Result<()> {        use std::os::fd::AsRawFd;        wait_for_fd_ready(self.stream.as_raw_fd(), events, self.deadline)    }}#[cfg(unix)]fn connect_unix_stream_before_deadline(    socket_path: &Path,    deadline: Instant,) -> std::io::Result<std::os::unix::net::UnixStream> {    use std::os::fd::AsRawFd;    use std::os::fd::FromRawFd;    use std::os::fd::IntoRawFd;    use std::os::fd::OwnedFd;    validate_unix_socket_path(socket_path)?;    let (addr, addr_len) = unix_socket_addr(socket_path)?;    let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0) };    if fd < 0 {        return Err(std::io::Error::last_os_error());    }    let fd = unsafe { OwnedFd::from_raw_fd(fd) };    set_fd_close_on_exec(fd.as_raw_fd())?;    set_fd_nonblocking(fd.as_raw_fd())?;    let result = unsafe {        libc::connect(            fd.as_raw_fd(),            &addr as *const libc::sockaddr_un as *const libc::sockaddr,            addr_len,        )    };    if result != 0 {        let error = std::io::Error::last_os_error();        if !is_in_progress_connect_error(&error) {            return Err(error);        }        wait_for_fd_ready(fd.as_raw_fd(), libc::POLLOUT, deadline)?;        let socket_error = socket_error(fd.as_raw_fd())?;        if socket_error != 0 {            return Err(std::io::Error::from_raw_os_error(socket_error));        }    }    Ok(unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd.into_raw_fd()) })}#[cfg(unix)]fn unix_socket_addr(socket_path: &Path) -> std::io::Result<(libc::sockaddr_un, libc::socklen_t)> {    use std::os::unix::ffi::OsStrExt;    let path_bytes = socket_path.as_os_str().as_bytes();    if path_bytes.contains(&0) {        return Err(std::io::Error::new(            std::io::ErrorKind::InvalidInput,            "IDE context Unix socket path contains a nul byte",        ));    }    let mut addr = unsafe { std::mem::zeroed::<libc::sockaddr_un>() };    if path_bytes.len() >= addr.sun_path.len() {        return Err(std::io::Error::new(            std::io::ErrorKind::InvalidInput,            "IDE context Unix socket path is too long",        ));    }    addr.sun_family = libc::AF_UNIX as libc::sa_family_t;    for (slot, byte) in addr.sun_path.iter_mut().zip(path_bytes) {        *slot = *byte as libc::c_char;    }    let addr_len =        std::mem::size_of::<libc::sockaddr_un>() - addr.sun_path.len() + path_bytes.len() + 1;    #[cfg(any(        target_os = "macos",        target_os = "freebsd",        target_os = "openbsd",        target_os = "netbsd",        target_os = "dragonfly"    ))]    {        addr.sun_len = u8::try_from(addr_len).map_err(|_| {            std::io::Error::new(                std::io::ErrorKind::InvalidInput,                "IDE context Unix socket address is too long",            )        })?;    }    let addr_len = libc::socklen_t::try_from(addr_len).map_err(|_| {        std::io::Error::new(            std::io::ErrorKind::InvalidInput,            "IDE context Unix socket address is too long",        )    })?;    Ok((addr, addr_len))}#[cfg(unix)]fn set_fd_close_on_exec(fd: libc::c_int) -> std::io::Result<()> {    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };    if flags < 0 {        return Err(std::io::Error::last_os_error());    }    let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };    if result < 0 {        return Err(std::io::Error::last_os_error());    }    Ok(())}#[cfg(unix)]fn set_fd_nonblocking(fd: libc::c_int) -> std::io::Result<()> {    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };    if flags < 0 {        return Err(std::io::Error::last_os_error());    }    let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };    if result < 0 {        return Err(std::io::Error::last_os_error());    }    Ok(())}#[cfg(unix)]fn is_in_progress_connect_error(error: &std::io::Error) -> bool {    matches!(        error.raw_os_error(),        Some(code)            if code == libc::EINPROGRESS                || code == libc::EALREADY                || code == libc::EWOULDBLOCK                || code == libc::EINTR    )}#[cfg(unix)]fn socket_error(fd: libc::c_int) -> std::io::Result<libc::c_int> {    let mut socket_error = 0;    let mut socket_error_len = libc::socklen_t::try_from(std::mem::size_of::<libc::c_int>())        .map_err(|_| {            std::io::Error::new(                std::io::ErrorKind::InvalidInput,                "invalid socket error length",            )        })?;    let result = unsafe {        libc::getsockopt(            fd,            libc::SOL_SOCKET,            libc::SO_ERROR,            &mut socket_error as *mut _ as *mut libc::c_void,            &mut socket_error_len,        )    };    if result != 0 {        return Err(std::io::Error::last_os_error());    }    Ok(socket_error)}#[cfg(unix)]fn remaining_timeout(deadline: Instant) -> std::io::Result<Duration> {    deadline        .checked_duration_since(Instant::now())        .filter(|duration| !duration.is_zero())        .ok_or_else(deadline_timeout_io_error)}#[cfg(unix)]fn remaining_timeout_ms(deadline: Instant) -> std::io::Result<libc::c_int> {    let millis = remaining_timeout(deadline)?.as_millis().max(1);    Ok(libc::c_int::try_from(millis).unwrap_or(libc::c_int::MAX))}#[cfg(unix)]fn wait_for_fd_ready(    fd: libc::c_int,    events: libc::c_short,    deadline: Instant,) -> std::io::Result<()> {    loop {        // Keep deadline handling in user space. Some macOS Unix socket environments reject        // SO_RCVTIMEO/SO_SNDTIMEO, but poll works consistently for our request-scoped timeout.        let mut poll_fd = libc::pollfd {            fd,            events,            revents: 0,        };        let result = unsafe { libc::poll(&mut poll_fd, 1, remaining_timeout_ms(deadline)?) };        if result == 0 {            return Err(deadline_timeout_io_error());        }        if result < 0 {            let error = std::io::Error::last_os_error();            if error.kind() == std::io::ErrorKind::Interrupted {                continue;            }            return Err(error);        }        if poll_fd.revents & libc::POLLNVAL != 0 {            return Err(std::io::Error::new(                std::io::ErrorKind::InvalidInput,                "invalid IDE context Unix socket",            ));        }        if poll_fd.revents & (events | libc::POLLERR | libc::POLLHUP) != 0 {            return Ok(());        }    }}#[cfg(unix)]impl std::io::Read for UnixDeadlineStream {    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {        if buf.is_empty() {            return Ok(0);        }        loop {            self.wait_for_ready(libc::POLLIN)?;            match self.stream.read(buf) {                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}                result => return result,            }        }    }}#[cfg(unix)]impl std::io::Write for UnixDeadlineStream {    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {        if buf.is_empty() {            return Ok(0);        }        loop {            self.wait_for_ready(libc::POLLOUT)?;            match self.stream.write(buf) {                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}                result => return result,            }        }    }    fn flush(&mut self) -> std::io::Result<()> {        self.wait_for_ready(libc::POLLOUT)?;        self.stream.flush()    }}#[cfg(unix)]fn validate_unix_socket_path(socket_path: &Path) -> std::io::Result<()> {    use std::os::unix::fs::FileTypeExt;    use std::os::unix::fs::MetadataExt;    use std::os::unix::fs::PermissionsExt;    let uid = unsafe { libc::getuid() };    let parent = socket_path.parent().ok_or_else(|| {        std::io::Error::new(            std::io::ErrorKind::PermissionDenied,            "IDE context socket has no parent directory",        )    })?;    let parent_metadata = std::fs::symlink_metadata(parent)?;    if !parent_metadata.is_dir() || parent_metadata.uid() != uid {        return Err(permission_denied_io_error(            "IDE context socket directory is not owned by the current user",        ));    }    if parent_metadata.permissions().mode() & 0o022 != 0 {        return Err(permission_denied_io_error(            "IDE context socket directory is writable by other users",        ));    }    let socket_metadata = std::fs::symlink_metadata(socket_path)?;    if !socket_metadata.file_type().is_socket() || socket_metadata.uid() != uid {        return Err(permission_denied_io_error(            "IDE context socket is not owned by the current user",        ));    }    Ok(())}#[cfg(any(target_os = "linux", target_os = "android"))]fn validate_unix_peer_owner(stream: &std::os::unix::net::UnixStream) -> std::io::Result<()> {    use std::os::fd::AsRawFd;    let mut credentials = unsafe { std::mem::zeroed::<libc::ucred>() };    let mut credentials_len: libc::socklen_t =        std::mem::size_of::<libc::ucred>().try_into().map_err(|_| {            std::io::Error::new(                std::io::ErrorKind::InvalidInput,                "invalid peer credential length",            )        })?;    let result = unsafe {        libc::getsockopt(            stream.as_raw_fd(),            libc::SOL_SOCKET,            libc::SO_PEERCRED,            &mut credentials as *mut _ as *mut libc::c_void,            &mut credentials_len,        )    };    if result != 0 {        return Err(std::io::Error::last_os_error());    }    ensure_peer_uid_matches_current_user(credentials.uid)}#[cfg(any(    target_os = "macos",    target_os = "freebsd",    target_os = "openbsd",    target_os = "netbsd",    target_os = "dragonfly"))]fn validate_unix_peer_owner(stream: &std::os::unix::net::UnixStream) -> std::io::Result<()> {    use std::os::fd::AsRawFd;    let mut peer_uid: libc::uid_t = 0;    let mut peer_gid: libc::gid_t = 0;    let result = unsafe { libc::getpeereid(stream.as_raw_fd(), &mut peer_uid, &mut peer_gid) };    if result != 0 {        return Err(std::io::Error::last_os_error());    }    ensure_peer_uid_matches_current_user(peer_uid)}#[cfg(all(    unix,    not(any(        target_os = "linux",        target_os = "android",        target_os = "macos",        target_os = "freebsd",        target_os = "openbsd",        target_os = "netbsd",        target_os = "dragonfly"    ))))]fn validate_unix_peer_owner(_stream: &std::os::unix::net::UnixStream) -> std::io::Result<()> {    Ok(())}#[cfg(unix)]fn ensure_peer_uid_matches_current_user(peer_uid: libc::uid_t) -> std::io::Result<()> {    if peer_uid != unsafe { libc::getuid() } {        return Err(permission_denied_io_error(            "IDE context provider is not owned by the current user",        ));    }    Ok(())}#[cfg(windows)]fn connect_stream(    socket_path: PathBuf,    deadline: Instant,) -> Result<IdeContextStream, IdeContextError> {    super::windows_pipe::WindowsPipeStream::connect(socket_path, deadline)        .map_err(IdeContextError::Connect)}#[cfg(any(unix, windows))]fn answer_unsupported_request<T: std::io::Write + ?Sized>(    stream: &mut T,    message: &Value,) -> Result<(), IdeContextError> {    if let Some(inbound_request_id) = message.get("requestId").and_then(Value::as_str) {        let response = json!({            "type": "response",            "requestId": inbound_request_id,            "resultType": "error",            "error": "no-handler-for-request",        });        write_frame(stream, &response).map_err(IdeContextError::Send)?;    }    Ok(())}#[cfg(any(unix, windows))]fn fetch_ide_context_from_stream(    stream: &mut IdeContextStream,    workspace_root: &Path,    deadline: Instant,) -> Result<IdeContext, IdeContextError> {    let request_id = uuid::Uuid::new_v4().to_string();    write_ide_context_request(stream, &request_id, workspace_root)        .map_err(IdeContextError::Send)?;    let response = read_response_frame(stream, &request_id, deadline)?;    extract_ide_context(response)}#[cfg(any(unix, windows))]fn write_ide_context_request<T: std::io::Write + ?Sized>(    stream: &mut T,    request_id: &str,    workspace_root: &Path,) -> std::io::Result<()> {    let ide_context_request = json!({        "type": "request",        "requestId": request_id,        "sourceClientId": TUI_SOURCE_CLIENT_ID,        "version": 0,        "method": "ide-context",        "params": {            "workspaceRoot": workspace_root.to_string_lossy(),        },    });    write_frame(stream, &ide_context_request)}#[cfg(any(unix, windows))]fn write_frame<T: std::io::Write + ?Sized>(stream: &mut T, message: &Value) -> std::io::Result<()> {    let payload = serde_json::to_vec(message).map_err(|err| {        std::io::Error::new(            std::io::ErrorKind::InvalidInput,            format!("invalid IDE context JSON message: {err}"),        )    })?;    let payload_len = u32::try_from(payload.len()).map_err(|_| {        std::io::Error::new(            std::io::ErrorKind::InvalidInput,            "IDE context payload exceeds u32 length",        )    })?;    stream.write_all(&payload_len.to_le_bytes())?;    stream.write_all(&payload)?;    stream.flush()}#[cfg(any(unix, windows))]fn read_frame<T: std::io::Read + ?Sized>(    stream: &mut T,    deadline: Instant,) -> Result<Value, IdeContextError> {    let mut len_bytes = [0_u8; 4];    read_exact_before_deadline(stream, &mut len_bytes, deadline)?;    let len = u32::from_le_bytes(len_bytes) as usize;    if len > MAX_IPC_FRAME_BYTES {        return Err(IdeContextError::ResponseTooLarge);    }    let mut payload = vec![0_u8; len];    read_exact_before_deadline(stream, &mut payload, deadline)?;    serde_json::from_slice(&payload)        .map_err(|err| IdeContextError::InvalidResponse(format!("invalid JSON payload: {err}")))}#[cfg(any(unix, windows))]fn read_exact_before_deadline<T: std::io::Read + ?Sized>(    stream: &mut T,    buf: &mut [u8],    deadline: Instant,) -> Result<(), IdeContextError> {    // std::io::Read::read_exact has no way to observe our request deadline between partial reads.    // Keep the frame header and payload under the same budget as the surrounding response wait.    let mut read_so_far = 0;    while read_so_far < buf.len() {        ensure_deadline_not_expired(deadline)?;        match stream.read(&mut buf[read_so_far..]) {            Ok(0) => {                return Err(IdeContextError::Read(std::io::Error::new(                    std::io::ErrorKind::UnexpectedEof,                    "failed to fill whole IDE context frame",                )));            }            Ok(bytes_read) => {                read_so_far += bytes_read;            }            Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}            Err(error) => return Err(IdeContextError::Read(error)),        }    }    ensure_deadline_not_expired(deadline)}#[cfg(any(unix, windows))]fn read_response_frame(    stream: &mut IdeContextStream,    request_id: &str,    deadline: Instant,) -> Result<Value, IdeContextError> {    loop {        ensure_deadline_not_expired(deadline)?;        stream.set_deadline(deadline);        let message = read_frame(stream, deadline)?;        match message.get("type").and_then(Value::as_str) {            Some("response") => {                if message.get("requestId").and_then(Value::as_str) == Some(request_id) {                    return Ok(message);                }            }            Some("broadcast") => {}            Some("client-discovery-request") => {                if let Some(discovery_request_id) = message.get("requestId").and_then(Value::as_str)                {                    let response = json!({                        "type": "client-discovery-response",                        "requestId": discovery_request_id,                        "response": {                            "canHandle": false,                        },                    });                    write_frame(stream, &response).map_err(IdeContextError::Send)?;                }            }            Some("client-discovery-response") => {}            Some("request") => {                answer_unsupported_request(stream, &message)?;            }            Some(other) => {                return Err(IdeContextError::InvalidResponse(format!(                    "unexpected IDE context message type: {other}"                )));            }            None => {                return Err(IdeContextError::InvalidResponse(                    "IDE context message did not include a type".to_string(),                ));            }        }    }}#[cfg(any(unix, windows))]fn ensure_deadline_not_expired(deadline: Instant) -> Result<(), IdeContextError> {    if Instant::now() >= deadline {        return Err(timeout_error());    }    Ok(())}#[cfg(any(unix, windows))]fn timeout_error() -> IdeContextError {    IdeContextError::Read(deadline_timeout_io_error())}#[cfg(any(unix, windows))]fn deadline_timeout_io_error() -> std::io::Error {    std::io::Error::new(        std::io::ErrorKind::TimedOut,        "timed out waiting for IDE context",    )}#[cfg(unix)]fn permission_denied_io_error(message: &'static str) -> std::io::Error {    std::io::Error::new(std::io::ErrorKind::PermissionDenied, message)}#[cfg(any(unix, windows))]fn extract_ide_context(response: Value) -> Result<IdeContext, IdeContextError> {    ensure_success_response(&response)?;    let ide_context = response        .get("result")        .and_then(|result| result.get("ideContext"))        .cloned()        .ok_or_else(|| {            IdeContextError::InvalidResponse(                "ide-context response did not include result.ideContext".to_string(),            )        })?;    serde_json::from_value(ide_context)        .map_err(|err| IdeContextError::InvalidResponse(err.to_string()))}#[cfg(any(unix, windows))]fn ensure_success_response(response: &Value) -> Result<(), IdeContextError> {    match response.get("resultType").and_then(Value::as_str) {        Some("success") => Ok(()),        Some("error") => Err(IdeContextError::RequestFailed(            response                .get("error")                .and_then(Value::as_str)                .unwrap_or("unknown error")                .to_string(),        )),        _ => Err(IdeContextError::InvalidResponse(            "response did not include a success or error resultType".to_string(),        )),    }}#[cfg(all(test, unix))]mod tests {    use super::*;    #[cfg(unix)]    use pretty_assertions::assert_eq;    #[cfg(unix)]    fn test_deadline() -> Instant {        Instant::now() + Duration::from_secs(1)    }    #[cfg(unix)]    fn write_ide_context_response(        stream: &mut impl std::io::Write,        request_id: &str,        active_selection_content: &str,    ) {        if let Err(err) = write_frame(            stream,            &json!({                "type": "response",                "requestId": request_id,                "resultType": "success",                "method": "ide-context",                "handledByClientId": "vscode-client",                "result": {                    "type": "broadcast",                    "ideContext": {                        "activeFile": {                            "label": "lib.rs",                            "path": "src/lib.rs",                            "fsPath": "/repo/src/lib.rs",                            "selection": {                                "start": { "line": 0, "character": 0 },                                "end": { "line": 0, "character": 3 }                            },                            "activeSelectionContent": active_selection_content,                            "selections": []                        },                        "openTabs": []                    }                }            }),        ) {            panic!("write ide-context response failed: {err}");        }    }    #[cfg(unix)]    #[test]    fn unix_deadline_stream_uses_remaining_deadline_for_blocking_reads() {        use std::os::unix::net::UnixStream;        let (client, _server) = UnixStream::pair().expect("create unix stream pair");        let mut stream =            UnixDeadlineStream::new(client, Instant::now() + Duration::from_millis(50));        let start = Instant::now();        let mut buf = [0_u8; 1];        let err = std::io::Read::read(&mut stream, &mut buf)            .expect_err("read should time out at the request deadline");        assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);        assert!(start.elapsed() < Duration::from_secs(2));    }    #[cfg(unix)]    #[test]    fn validate_unix_socket_path_rejects_unsafe_parent_directory() {        use std::os::unix::fs::PermissionsExt;        use std::os::unix::net::UnixListener;        let tempdir = tempfile::tempdir().expect("tempdir");        std::fs::set_permissions(tempdir.path(), std::fs::Permissions::from_mode(0o777))            .expect("set unsafe permissions");        let socket_path = tempdir.path().join("codex-ipc.sock");        let _listener = UnixListener::bind(&socket_path).expect("bind socket");        let err = validate_unix_socket_path(&socket_path)            .expect_err("world-writable parent directory should be rejected");        assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);    }    #[cfg(unix)]    #[test]    fn fetch_ide_context_uses_unregistered_request_route() {        use std::os::unix::net::UnixListener;        use std::thread;        let tempdir = tempfile::tempdir().expect("tempdir");        let socket_path = tempdir.path().join("codex-ipc.sock");        let listener = UnixListener::bind(&socket_path).expect("bind socket");        let server = thread::spawn(move || {            let (mut stream, _) = listener.accept().expect("accept");            let ide_context = read_frame(&mut stream, test_deadline()).expect("read ide-context");            assert_eq!(                ide_context.get("method").and_then(Value::as_str),                Some("ide-context")            );            assert_eq!(                ide_context.get("sourceClientId").and_then(Value::as_str),                Some(TUI_SOURCE_CLIENT_ID)            );            assert_eq!(                ide_context                    .get("params")                    .and_then(|params| params.get("workspaceRoot"))                    .and_then(Value::as_str),                Some("/repo")            );            let ide_context_request_id = ide_context                .get("requestId")                .and_then(Value::as_str)                .expect("ide-context request id");            write_frame(                &mut stream,                &json!({                    "type": "request",                    "requestId": "inbound-request",                    "sourceClientId": "vscode-client",                    "version": 0,                    "method": "unknown-method",                    "params": {}                }),            )            .expect("write inbound request before ide-context response");            let inbound_response = read_frame(&mut stream, test_deadline())                .expect("read inbound request response before ide-context response");            assert_eq!(                inbound_response,                json!({                    "type": "response",                    "requestId": "inbound-request",                    "resultType": "error",                    "error": "no-handler-for-request"                })            );            write_frame(                &mut stream,                &json!({                    "type": "client-discovery-request",                    "requestId": "discovery-request",                    "request": ide_context.clone(),                }),            )            .expect("write client discovery request");            let discovery_response =                read_frame(&mut stream, test_deadline()).expect("read client discovery response");            assert_eq!(                discovery_response.get("type").and_then(Value::as_str),                Some("client-discovery-response")            );            assert_eq!(                discovery_response.get("requestId").and_then(Value::as_str),                Some("discovery-request")            );            assert_eq!(                discovery_response                    .get("response")                    .and_then(|response| response.get("canHandle"))                    .and_then(Value::as_bool),                Some(false)            );            write_frame(                &mut stream,                &json!({                    "type": "broadcast",                    "method": "thread-stream-state-changed",                    "params": "x".repeat(2 * 1024 * 1024),                }),            )            .expect("write large broadcast");            write_ide_context_response(&mut stream, ide_context_request_id, "use");        });        let context =            fetch_ide_context_from_socket(socket_path, Path::new("/repo"), Duration::from_secs(1))                .expect("fetch ide context");        server.join().expect("server joins");        assert_eq!(            context                .active_file                .as_ref()                .map(|file| file.active_selection_content.as_str()),            Some("use")        );    }}