mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Add WebSocket transport to the code-mode host (#35078)
## What changed - Add a `--listen` option that accepts `stdio`, `stdio://`, or a `ws://IP:PORT` endpoint, while retaining stdio as the default. - Serve the existing length-prefixed protocol in binary WebSocket messages, with isolated connections, shared host limits, and a `/readyz` endpoint. - Reject browser-origin handshakes and contain malformed frames to the affected connection. ## Testing - Cover listen URL parsing and complete-frame encoding and decoding. - Exercise readiness, cell execution, tool callbacks, large frames, concurrent connections, malformed frames, and origin rejection through the WebSocket listener. GitOrigin-RevId: 01c8be4c6256b8ce4a3a0002440dcb3294e5f887
This commit is contained in:
committed by
copyberry
parent
f47f28cd0d
commit
0dfa778dae
@@ -8,7 +8,7 @@ use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// Maximum JSON payload size accepted for one IPC frame.
|
||||
/// Maximum JSON payload size accepted for one code-mode host frame.
|
||||
pub const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
/// A serialized IPC frame that has already passed the payload size limit.
|
||||
@@ -39,6 +39,55 @@ impl EncodedFrame {
|
||||
}
|
||||
Ok(Self { payload })
|
||||
}
|
||||
|
||||
/// Returns the complete length-prefixed representation of this frame.
|
||||
pub fn into_framed_bytes(self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(size_of::<u32>() + self.payload.len());
|
||||
bytes.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&self.payload);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Decodes exactly one complete length-prefixed frame.
|
||||
pub fn decode_framed<T>(bytes: &[u8]) -> io::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let length_bytes: [u8; size_of::<u32>()] = bytes
|
||||
.get(..size_of::<u32>())
|
||||
.and_then(|length_bytes| length_bytes.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"code-mode IPC frame is missing its length prefix",
|
||||
)
|
||||
})?;
|
||||
let length = u32::from_le_bytes(length_bytes) as usize;
|
||||
if length > MAX_FRAME_BYTES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("code-mode IPC frame length {length} exceeds {MAX_FRAME_BYTES} bytes"),
|
||||
));
|
||||
}
|
||||
|
||||
let payload = &bytes[size_of::<u32>()..];
|
||||
if payload.len() != length {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"code-mode IPC frame declares {length} payload bytes but contains {}",
|
||||
payload.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
serde_json::from_slice(payload).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("failed to decode code-mode IPC frame: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes JSON messages prefixed by a four-byte little-endian payload length.
|
||||
|
||||
@@ -3,10 +3,43 @@ use serde_json::json;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use super::EncodedFrame;
|
||||
use super::FramedReader;
|
||||
use super::FramedWriter;
|
||||
use super::MAX_FRAME_BYTES;
|
||||
|
||||
#[test]
|
||||
fn complete_frame_round_trips_without_a_byte_stream() {
|
||||
let value = json!({"type": "session/open", "sessionId": "session-1"});
|
||||
let bytes = EncodedFrame::encode(&value)
|
||||
.expect("encode frame")
|
||||
.into_framed_bytes();
|
||||
|
||||
assert_eq!(
|
||||
EncodedFrame::decode_framed::<serde_json::Value>(&bytes).expect("decode frame"),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_frame_rejects_truncated_and_trailing_payloads() {
|
||||
let value = json!({"value": 1});
|
||||
let bytes = EncodedFrame::encode(&value)
|
||||
.expect("encode frame")
|
||||
.into_framed_bytes();
|
||||
|
||||
let truncated = &bytes[..bytes.len() - 1];
|
||||
let truncated_error = EncodedFrame::decode_framed::<serde_json::Value>(truncated)
|
||||
.expect_err("truncated frame should fail");
|
||||
assert_eq!(truncated_error.kind(), std::io::ErrorKind::InvalidData);
|
||||
|
||||
let mut trailing = bytes;
|
||||
trailing.push(0);
|
||||
let trailing_error = EncodedFrame::decode_framed::<serde_json::Value>(&trailing)
|
||||
.expect_err("frame with trailing bytes should fail");
|
||||
assert_eq!(trailing_error.kind(), std::io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn frame_wire_format_is_little_endian_length_prefixed_json() {
|
||||
let (writer, mut reader) = tokio::io::duplex(/*max_buf_size*/ 128);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Messages and local IPC framing for the code-mode host boundary.
|
||||
//! Messages and framing for the code-mode host boundary.
|
||||
//!
|
||||
//! Protocol version 1 multiplexes session operations and delegate callbacks by
|
||||
//! request ID over one ordered connection. It defines no optional capabilities
|
||||
|
||||
Reference in New Issue
Block a user