mirror of
https://github.com/openai/codex.git
synced 2026-05-14 16:22:51 +00:00
### Why Remote streamable HTTP MCP needs a transport-shaped executor primitive before the MCP client can move network I/O to the executor. This layer keeps the executor unaware of MCP and gives later PRs an ordered streaming surface for response bodies. ### What - Add typed `http/request` and `http/request/bodyDelta` protocol payloads. - Add executor client helpers for buffered and streamed HTTP responses. - Route body-delta notifications to request-scoped streams with sequence validation and cleanup when a stream finishes or is dropped. - Document the new protocol constants, transport structs, public client methods, body-stream lifecycle, and request-scoped routing helpers. - Add in-memory JSON-RPC client coverage for streamed HTTP response-body notifications, with comments spelling out what the test proves and each setup/exercise/assert phase. ### Stack 1. #18581 protocol 2. #18582 runner 3. #18583 RMCP client 4. #18584 manager wiring and local/remote coverage ### Verification - `just fmt` - `cargo check -p codex-exec-server -p codex-rmcp-client --tests` - `cargo check -p codex-core --test all` compile-only - `git diff --check` - Online full CI is running from the `full-ci` branch, including the remote Rust test job. Co-authored-by: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::FileSystemSandboxContext;
|
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
use codex_config::types::ShellEnvironmentPolicyInherit;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::ProcessId;
|
|
|
|
pub const INITIALIZE_METHOD: &str = "initialize";
|
|
pub const INITIALIZED_METHOD: &str = "initialized";
|
|
pub const EXEC_METHOD: &str = "process/start";
|
|
pub const EXEC_READ_METHOD: &str = "process/read";
|
|
pub const EXEC_WRITE_METHOD: &str = "process/write";
|
|
pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
|
|
pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
|
|
pub const EXEC_EXITED_METHOD: &str = "process/exited";
|
|
pub const EXEC_CLOSED_METHOD: &str = "process/closed";
|
|
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
|
|
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
|
|
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
|
|
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
|
|
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
|
|
pub const FS_REMOVE_METHOD: &str = "fs/remove";
|
|
pub const FS_COPY_METHOD: &str = "fs/copy";
|
|
/// JSON-RPC request method for executor-side HTTP requests.
|
|
pub const HTTP_REQUEST_METHOD: &str = "http/request";
|
|
/// JSON-RPC notification method for streamed executor HTTP response bodies.
|
|
pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct ByteChunk(#[serde(with = "base64_bytes")] pub Vec<u8>);
|
|
|
|
impl ByteChunk {
|
|
pub fn into_inner(self) -> Vec<u8> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl From<Vec<u8>> for ByteChunk {
|
|
fn from(value: Vec<u8>) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeParams {
|
|
pub client_name: String,
|
|
#[serde(default)]
|
|
pub resume_session_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeResponse {
|
|
pub session_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecParams {
|
|
/// Client-chosen logical process handle scoped to this connection/session.
|
|
/// This is a protocol key, not an OS pid.
|
|
pub process_id: ProcessId,
|
|
pub argv: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
#[serde(default)]
|
|
pub env_policy: Option<ExecEnvPolicy>,
|
|
pub env: HashMap<String, String>,
|
|
pub tty: bool,
|
|
/// Keep non-tty stdin writable through `process/write`.
|
|
#[serde(default)]
|
|
pub pipe_stdin: bool,
|
|
pub arg0: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecEnvPolicy {
|
|
pub inherit: ShellEnvironmentPolicyInherit,
|
|
pub ignore_default_excludes: bool,
|
|
pub exclude: Vec<String>,
|
|
pub r#set: HashMap<String, String>,
|
|
pub include_only: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecResponse {
|
|
pub process_id: ProcessId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadParams {
|
|
pub process_id: ProcessId,
|
|
pub after_seq: Option<u64>,
|
|
pub max_bytes: Option<usize>,
|
|
pub wait_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProcessOutputChunk {
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadResponse {
|
|
pub chunks: Vec<ProcessOutputChunk>,
|
|
pub next_seq: u64,
|
|
pub exited: bool,
|
|
pub exit_code: Option<i32>,
|
|
pub closed: bool,
|
|
pub failure: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteParams {
|
|
pub process_id: ProcessId,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum WriteStatus {
|
|
Accepted,
|
|
UnknownProcess,
|
|
StdinClosed,
|
|
Starting,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteResponse {
|
|
pub status: WriteStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateParams {
|
|
pub process_id: ProcessId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateResponse {
|
|
pub running: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadFileParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadFileResponse {
|
|
pub data_base64: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsWriteFileParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub data_base64: String,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsWriteFileResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCreateDirectoryParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub recursive: Option<bool>,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCreateDirectoryResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsGetMetadataParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsGetMetadataResponse {
|
|
pub is_directory: bool,
|
|
pub is_file: bool,
|
|
pub is_symlink: bool,
|
|
pub created_at_ms: i64,
|
|
pub modified_at_ms: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryEntry {
|
|
pub file_name: String,
|
|
pub is_directory: bool,
|
|
pub is_file: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryResponse {
|
|
pub entries: Vec<FsReadDirectoryEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsRemoveParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub recursive: Option<bool>,
|
|
pub force: Option<bool>,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsRemoveResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCopyParams {
|
|
pub source_path: AbsolutePathBuf,
|
|
pub destination_path: AbsolutePathBuf,
|
|
pub recursive: bool,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCopyResponse {}
|
|
|
|
/// HTTP header represented in the executor protocol.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpHeader {
|
|
/// Header name as it appears on the HTTP wire.
|
|
pub name: String,
|
|
/// Header value after UTF-8 conversion.
|
|
pub value: String,
|
|
}
|
|
|
|
/// Executor-side HTTP request envelope.
|
|
///
|
|
/// This intentionally stays transport-shaped rather than MCP-shaped so callers
|
|
/// can use it for Streamable HTTP, OAuth discovery, and future executor-owned
|
|
/// HTTP probes without introducing one protocol method per higher-level use.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestParams {
|
|
/// HTTP method, for example `GET`, `POST`, or `DELETE`.
|
|
pub method: String,
|
|
/// Absolute `http://` or `https://` URL.
|
|
pub url: String,
|
|
/// Ordered request headers. Repeated header names are preserved.
|
|
#[serde(default)]
|
|
pub headers: Vec<HttpHeader>,
|
|
/// Optional request body bytes.
|
|
#[serde(default, rename = "bodyBase64")]
|
|
pub body: Option<ByteChunk>,
|
|
/// Optional request timeout in milliseconds.
|
|
#[serde(default)]
|
|
pub timeout_ms: Option<u64>,
|
|
/// Caller-chosen stream id for `http/request/bodyDelta` notifications.
|
|
///
|
|
/// The id must remain unique on a connection until the terminal body delta
|
|
/// arrives, even if the caller stops reading the stream earlier.
|
|
#[serde(default)]
|
|
pub request_id: Option<String>,
|
|
/// Return after response headers and stream the response body as deltas.
|
|
#[serde(default)]
|
|
pub stream_response: bool,
|
|
}
|
|
|
|
/// HTTP response envelope returned from an executor `http/request` call.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestResponse {
|
|
/// Numeric HTTP response status code.
|
|
pub status: u16,
|
|
/// Ordered response headers. Repeated header names are preserved.
|
|
pub headers: Vec<HttpHeader>,
|
|
/// Buffered response body bytes. Empty when `streamResponse` is true.
|
|
#[serde(rename = "bodyBase64")]
|
|
pub body: ByteChunk,
|
|
}
|
|
|
|
/// Ordered response-body frame for `streamResponse` HTTP requests.
|
|
///
|
|
/// Headers are returned in the `http/request` response so the caller can choose
|
|
/// a parser immediately; body bytes then arrive on this notification stream.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestBodyDeltaNotification {
|
|
/// Request id from the streamed `http/request` call.
|
|
pub request_id: String,
|
|
/// Monotonic one-based body frame sequence number.
|
|
pub seq: u64,
|
|
/// Response-body bytes carried by this frame.
|
|
#[serde(rename = "deltaBase64")]
|
|
pub delta: ByteChunk,
|
|
/// Marks response-body EOF. No later deltas are expected for this request.
|
|
#[serde(default)]
|
|
pub done: bool,
|
|
/// Terminal stream error. Set only on the final notification.
|
|
#[serde(default)]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ExecOutputStream {
|
|
Stdout,
|
|
Stderr,
|
|
Pty,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOutputDeltaNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecExitedNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
pub exit_code: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecClosedNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
}
|
|
|
|
mod base64_bytes {
|
|
use super::BASE64_STANDARD;
|
|
use base64::Engine as _;
|
|
use serde::Deserialize;
|
|
use serde::Deserializer;
|
|
use serde::Serializer;
|
|
|
|
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let encoded = String::deserialize(deserializer)?;
|
|
BASE64_STANDARD
|
|
.decode(encoded)
|
|
.map_err(serde::de::Error::custom)
|
|
}
|
|
}
|