use std::collections::HashMap;use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;use codex_file_system::FileSystemSandboxContext;use codex_protocol::config_types::ShellEnvironmentPolicyInherit;use codex_utils_path_uri::PathUri;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_SIGNAL_METHOD: &str = "process/signal";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 ENVIRONMENT_INFO_METHOD: &str = "environment/info";pub const FS_READ_FILE_METHOD: &str = "fs/readFile";pub(crate) const FS_OPEN_METHOD: &str = "fs/open";pub(crate) const FS_READ_BLOCK_METHOD: &str = "fs/readBlock";pub(crate) const FS_CLOSE_METHOD: &str = "fs/close";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_CANONICALIZE_METHOD: &str = "fs/canonicalize";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,}/// Information about an execution/filesystem environment.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct EnvironmentInfo { pub shell: ShellInfo,}/// Shell detected for an execution/filesystem environment.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct ShellInfo { /// Stable shell name, for example `zsh`, `bash`, `powershell`, `sh`, or `cmd`. pub name: String, /// Target-native shell executable path or command name. Fallbacks such as `cmd.exe` need not /// be absolute, so this is not a [`PathUri`]. pub path: 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>, /// Working directory URI, interpreted using the exec-server host's path rules at launch time. pub cwd: PathUri, #[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, /// Optional process-visible argv0 override. Values such as `codex-linux-sandbox` are command /// names rather than paths, so this is not a [`PathUri`]. 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, Copy, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub enum ProcessSignal { Interrupt,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct SignalParams { pub process_id: ProcessId, pub signal: ProcessSignal,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct SignalResponse {}#[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: PathUri, 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 FsOpenParams { pub handle_id: String, pub path: PathUri, pub sandbox: Option<FileSystemSandboxContext>,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsOpenResponse { pub handle_id: String,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsReadBlockParams { pub handle_id: String, pub offset: u64, pub len: usize,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsReadBlockResponse { pub chunk: ByteChunk, pub eof: bool,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsCloseParams { pub handle_id: String,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsCloseResponse {}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsWriteFileParams { pub path: PathUri, 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: PathUri, 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: PathUri, 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 size: u64, pub created_at_ms: i64, pub modified_at_ms: i64,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsCanonicalizeParams { pub path: PathUri, pub sandbox: Option<FileSystemSandboxContext>,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsCanonicalizeResponse { pub path: PathUri,}#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct FsReadDirectoryParams { pub path: PathUri, 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: PathUri, 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: PathUri, pub destination_path: PathUri, 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>, /// 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<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. 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<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) }}#[cfg(test)]mod tests { use super::FsReadFileParams; use super::HttpRequestParams; use codex_file_system::FileSystemSandboxContext; use codex_protocol::models::PermissionProfile; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; #[test] fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris() { let legacy_path = std::env::current_dir() .expect("current directory") .join("legacy-file.txt"); let legacy_cwd = std::env::current_dir().expect("current directory"); let native_sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::default(), PathUri::from_path(&legacy_cwd).expect("cwd URI"), ); let mut legacy_sandbox = serde_json::to_value(&native_sandbox).expect("sandbox should serialize"); legacy_sandbox["cwd"] = serde_json::json!(legacy_cwd.to_string_lossy()); let params: FsReadFileParams = serde_json::from_value(serde_json::json!({ "path": legacy_path.to_string_lossy(), "sandbox": legacy_sandbox, })) .expect("legacy absolute path should deserialize"); let expected_sandbox = native_sandbox; let expected = FsReadFileParams { path: PathUri::from_path(legacy_path).expect("path URI"), sandbox: Some(expected_sandbox.clone()), }; assert_eq!(params, expected); assert_eq!( serde_json::to_value(params).expect("params should serialize"), serde_json::json!({ "path": expected.path.to_string(), "sandbox": serde_json::to_value(expected_sandbox) .expect("sandbox should serialize"), }) ); } #[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)) ); }}