[codex] extend code-mode host IPC transport (#30108)

## Summary

- add an `EncodedFrame` type so IPC payloads are serialized and
size-checked before entering bounded queues
- add the V1 `operation/cancel` client-to-host message
- pin the new wire shape with protocol tests

## Why

The process-owned code-mode host needs bounded, pre-encoded outbound
messages and a best-effort cancellation signal. Keeping these wire
primitives in a protocol-only change lets their compatibility contract
be reviewed independently from either endpoint.

## Stack

This is **1 of 4** in the process-owned code-mode session stack. The
next PR targets this branch.

## Validation

- `just test -p codex-code-mode-protocol` — 22 passed
- `just fix -p codex-code-mode-protocol`
- `just fmt`
This commit is contained in:
Channing Conger
2026-06-25 13:26:47 -07:00
committed by GitHub
Unverified
parent adccb464d0
commit 3b78f58fb2
4 changed files with 50 additions and 17 deletions
+37 -17
View File
@@ -11,6 +11,36 @@ use tokio::io::AsyncWriteExt;
/// Maximum JSON payload size accepted for one IPC frame.
pub const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
/// A serialized IPC frame that has already passed the payload size limit.
#[derive(Clone, Debug)]
pub struct EncodedFrame {
payload: Vec<u8>,
}
impl EncodedFrame {
pub fn encode<T>(message: &T) -> io::Result<Self>
where
T: Serialize,
{
let payload = serde_json::to_vec(message).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("failed to encode code-mode IPC frame: {err}"),
)
})?;
if payload.len() > MAX_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"code-mode IPC frame length {} exceeds {MAX_FRAME_BYTES} bytes",
payload.len()
),
));
}
Ok(Self { payload })
}
}
/// Decodes JSON messages prefixed by a four-byte little-endian payload length.
pub struct FramedReader<R> {
reader: R,
@@ -72,22 +102,12 @@ where
where
T: Serialize,
{
let payload = serde_json::to_vec(message).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("failed to encode code-mode IPC frame: {err}"),
)
})?;
if payload.len() > MAX_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"code-mode IPC frame length {} exceeds {MAX_FRAME_BYTES} bytes",
payload.len()
),
));
}
let length = u32::try_from(payload.len()).map_err(|_| {
self.write_frame(&EncodedFrame::encode(message)?).await
}
/// Writes and flushes a frame encoded before it entered an I/O queue.
pub async fn write_frame(&mut self, frame: &EncodedFrame) -> io::Result<()> {
let length = u32::try_from(frame.payload.len()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"code-mode IPC frame length exceeds u32",
@@ -95,7 +115,7 @@ where
})?;
self.writer.write_all(&length.to_le_bytes()).await?;
self.writer.write_all(&payload).await?;
self.writer.write_all(&frame.payload).await?;
self.writer.flush().await
}
}
@@ -365,6 +365,16 @@ fn client_to_host_v1_variants_are_pinned() {
}),
);
}
assert_wire_round_trip(
ClientToHost::CancelRequest {
id: request_id(/*value*/ 9),
},
json!({
"type": "operation/cancel",
"id": 9,
}),
);
}
#[test]
@@ -132,6 +132,8 @@ pub enum ClientToHost {
ClientHello(ClientHello),
#[serde(rename = "operation/request")]
Request { id: RequestId, request: HostRequest },
#[serde(rename = "operation/cancel")]
CancelRequest { id: RequestId },
#[serde(rename = "delegate/response")]
DelegateResponse {
id: DelegateRequestId,
@@ -11,6 +11,7 @@ mod message;
mod payload;
mod types;
pub use codec::EncodedFrame;
pub use codec::FramedReader;
pub use codec::FramedWriter;
pub use codec::MAX_FRAME_BYTES;