use std::collections::HashMap; use std::path::PathBuf; use crate::FileSystemSandboxContext; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_protocol::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); impl ByteChunk { pub fn into_inner(self) -> Vec { self.0 } } impl From> for ByteChunk { fn from(value: Vec) -> 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, } #[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, pub cwd: PathBuf, #[serde(default)] pub env_policy: Option, pub env: HashMap, pub tty: bool, /// Keep non-tty stdin writable through `process/write`. #[serde(default)] pub pipe_stdin: bool, pub arg0: Option, } #[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, pub r#set: HashMap, pub include_only: Vec, } #[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, pub max_bytes: Option, pub wait_ms: Option, } #[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, pub next_seq: u64, pub exited: bool, pub exit_code: Option, pub closed: bool, pub failure: Option, } #[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, } #[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, } #[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, pub sandbox: Option, } #[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, } #[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, } #[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, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FsRemoveParams { pub path: AbsolutePathBuf, pub recursive: Option, pub force: Option, pub sandbox: Option, } #[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, } #[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, /// Optional request body bytes. #[serde(default, rename = "bodyBase64")] pub body: Option, /// Request timeout in milliseconds. /// /// Omitted or `null` disables the timeout. A number applies that exact /// millisecond deadline. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, /// 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. Buffered /// requests still send an id so callers can keep one consistent request /// envelope shape. pub request_id: 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, /// 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, } #[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(bytes: &[u8], serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&BASE64_STANDARD.encode(bytes)) } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let encoded = String::deserialize(deserializer)?; BASE64_STANDARD .decode(encoded) .map_err(serde::de::Error::custom) } } #[cfg(test)] mod tests { use super::HttpRequestParams; use pretty_assertions::assert_eq; #[test] fn http_request_timeout_treats_omitted_and_null_as_no_timeout() { let omitted: HttpRequestParams = serde_json::from_value(serde_json::json!({ "method": "GET", "url": "https://example.test", "requestId": "req-omitted-timeout", })) .expect("omitted timeout should deserialize"); let null_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({ "method": "GET", "url": "https://example.test", "requestId": "req-null-timeout", "timeoutMs": null, })) .expect("null timeout should deserialize"); let explicit_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({ "method": "GET", "url": "https://example.test", "requestId": "req-explicit-timeout", "timeoutMs": 1234, })) .expect("numeric timeout should deserialize"); assert_eq!( (omitted.request_id.as_str(), omitted.timeout_ms), ("req-omitted-timeout", None) ); assert_eq!( (null_timeout.request_id.as_str(), null_timeout.timeout_ms), ("req-null-timeout", None) ); assert_eq!( ( explicit_timeout.request_id.as_str(), explicit_timeout.timeout_ms ), ("req-explicit-timeout", Some(1234)) ); } }