protocol: separate app and exec RPC ownership (#29714)

## Why

The app-server and exec-server expose separate JSON-RPC APIs, but
exec-server currently sources its serialized protocol and envelope types
through app-server-oriented code. Giving each API an explicit owner
makes the crate boundary legible without introducing shared generic
envelopes.

## What changed

- Added `codex-exec-server-protocol` to own exec DTOs, process IDs, and
JSON-RPC envelopes.
- Updated exec-server clients, transports, handlers, and tests to use
the new crate.
- Exposed app-server's existing JSON-RPC types through a public `rpc`
module while retaining root re-exports.
- Preserved existing wire shapes, including exec `PathUri` behavior.

## Stack

This is PR 1 of 6. Next: [PR
#29721](https://github.com/openai/codex/pull/29721), which moves auth
mode below the app wire boundary.

## Validation

- Exec-server protocol and server coverage passed in the focused
protocol test runs.
- App-server protocol schema fixtures passed.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-23 15:37:31 -07:00
committed by GitHub
Unverified
parent 220f5b76b2
commit 829f5b6b59
45 changed files with 255 additions and 131 deletions
+4 -4
View File
@@ -9,7 +9,7 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use arc_swap::ArcSwap;
use codex_app_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCNotification;
use futures::FutureExt;
use futures::future::BoxFuture;
use serde_json::Value;
@@ -1187,9 +1187,9 @@ async fn handle_server_notification(
#[cfg(test)]
mod tests {
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCResponse;
use futures::SinkExt;
use futures::StreamExt;
use pretty_assertions::assert_eq;
@@ -8,8 +8,8 @@
use std::error::Error as StdError;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_exec_server_protocol::JSONRPCErrorError;
use futures::FutureExt;
use futures::StreamExt;
use futures::future::BoxFuture;
+3 -5
View File
@@ -7,7 +7,7 @@ use std::time::Duration;
use axum::extract::ws::Message as AxumWebSocketMessage;
use axum::extract::ws::WebSocket as AxumWebSocket;
use codex_app_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCMessage;
use futures::Sink;
use futures::SinkExt;
use futures::Stream;
@@ -597,8 +597,8 @@ mod tests {
use std::task::Context;
use std::task::Poll;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCRequest;
use codex_exec_server_protocol::RequestId;
use futures::channel::mpsc as futures_mpsc;
use futures::task::AtomicWaker;
use tokio::net::TcpListener;
@@ -667,7 +667,6 @@ mod tests {
id: RequestId::Integer(1),
method: "test".to_string(),
params: None,
trace: None,
});
server_websocket
@@ -732,7 +731,6 @@ mod tests {
id: RequestId::Integer(1),
method: "test".to_string(),
params: None,
trace: None,
})
}
-23
View File
@@ -23,12 +23,9 @@ use crate::local_file_system::LocalFileSystem;
use crate::local_process::LocalProcess;
use crate::process::ExecBackend;
use crate::protocol::EnvironmentInfo;
use crate::protocol::ShellInfo;
use crate::remote::NoiseRendezvousEnvironmentConfig;
use crate::remote_file_system::RemoteFileSystem;
use crate::remote_process::RemoteProcess;
use codex_shell_command::shell_detect::DetectedShell;
use codex_utils_path_uri::PathUri;
use tokio_util::task::AbortOnDropHandle;
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
@@ -605,26 +602,6 @@ impl Environment {
}
}
impl EnvironmentInfo {
pub(crate) fn local() -> Self {
Self {
shell: codex_shell_command::shell_detect::default_user_shell().into(),
cwd: std::env::current_dir()
.ok()
.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()),
}
}
}
impl From<DetectedShell> for ShellInfo {
fn from(shell: DetectedShell) -> Self {
Self {
name: shell.name().to_string(),
path: shell.shell_path.to_string_lossy().into_owned(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
+1 -1
View File
@@ -1,6 +1,6 @@
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use serde::Deserialize;
use serde::Serialize;
use tokio::io;
+1 -1
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
+3 -3
View File
@@ -15,9 +15,7 @@ mod local_process;
mod noise_channel;
mod noise_relay;
mod process;
mod process_id;
mod process_sandbox;
mod protocol;
mod regular_file;
mod relay;
mod relay_proto;
@@ -29,6 +27,8 @@ mod runtime_paths;
mod sandboxed_file_system;
mod server;
use codex_exec_server_protocol as protocol;
pub use client::ExecServerClient;
pub use client::ExecServerError;
pub use client::http_client::HttpResponseBodyStream;
@@ -39,6 +39,7 @@ pub use client_api::NoiseRendezvousConnectArgs;
pub use client_api::NoiseRendezvousConnectBundle;
pub use client_api::NoiseRendezvousConnectProvider;
pub use client_api::RemoteExecServerConnectArgs;
pub use codex_exec_server_protocol::ProcessId;
pub use codex_file_system::CopyOptions;
pub use codex_file_system::CreateDirectoryOptions;
pub use codex_file_system::ExecutorFileSystem;
@@ -78,7 +79,6 @@ pub use process::ExecProcessEvent;
pub use process::ExecProcessEventReceiver;
pub use process::ExecProcessFuture;
pub use process::StartedExecProcess;
pub use process_id::ProcessId;
pub use protocol::ByteChunk;
pub use protocol::EnvironmentInfo;
pub use protocol::ExecClosedNotification;
+1 -1
View File
@@ -7,7 +7,7 @@ use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_protocol::config_types::EnvironmentVariablePattern;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -1,9 +1,9 @@
use std::time::Duration;
use anyhow::Result;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::RequestId;
use tokio::sync::mpsc;
use tokio::time::timeout;
@@ -1,4 +1,4 @@
use codex_app_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCMessage;
use crate::ExecServerError;
@@ -1,5 +1,5 @@
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCNotification;
use pretty_assertions::assert_eq;
use super::JsonRpcMessageDecoder;
-74
View File
@@ -1,74 +0,0 @@
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProcessId(String);
impl ProcessId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl Deref for ProcessId {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl Borrow<str> for ProcessId {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for ProcessId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ProcessId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<String> for ProcessId {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for ProcessId {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl From<&String> for ProcessId {
fn from(value: &String) -> Self {
Self(value.clone())
}
}
impl From<ProcessId> for String {
fn from(value: ProcessId) -> Self {
value.0
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
use codex_protocol::models::PermissionProfile;
-662
View File
@@ -1,662 +0,0 @@
use std::collections::HashMap;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_file_system::FileSystemSandboxContext;
use codex_network_proxy::ManagedNetworkSandboxContext;
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,
/// Working directory inherited by the exec-server process.
#[serde(default)]
pub cwd: Option<PathUri>,
}
/// 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>,
/// Portable sandbox intent. Concrete wrapper argv is resolved by the exec-server.
#[serde(default)]
pub sandbox: Option<FileSystemSandboxContext>,
/// Whether the eventual executor-side sandbox must enforce managed networking.
#[serde(default)]
pub enforce_managed_network: bool,
/// Optional details for enforcing managed networking without a live proxy object.
///
/// When `enforce_managed_network` is true and these details are absent, the executor must
/// continue to fail closed. This preserves compatibility with older clients.
#[serde(default)]
pub managed_network: Option<ManagedNetworkSandboxContext>,
}
#[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>,
/// Whether the executor classified the process failure as a sandbox denial.
#[serde(default)]
pub sandbox_denied: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteParams {
pub process_id: ProcessId,
pub chunk: ByteChunk,
pub write_id: String,
}
#[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::EnvironmentInfo;
use super::ExecParams;
use super::FsReadFileParams;
use super::HttpRequestParams;
use super::ProcessId;
use super::ShellInfo;
use codex_file_system::FileSystemSandboxContext;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_protocol::models::PermissionProfile;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
#[test]
fn exec_params_managed_network_context_round_trips_and_defaults_for_legacy_peers() {
let cwd =
PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
.expect("cwd URI");
let params = ExecParams {
process_id: ProcessId::from("managed-network"),
argv: vec!["true".to_string()],
cwd,
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
enforce_managed_network: true,
managed_network: Some(ManagedNetworkSandboxContext {
loopback_ports: vec![43123, 48081],
allow_local_binding: false,
}),
};
let mut serialized = serde_json::to_value(&params).expect("serialize exec params");
assert_eq!(
serialized["managedNetwork"],
serde_json::json!({
"loopbackPorts": [43123, 48081],
"allowLocalBinding": false,
})
);
let round_trip: ExecParams =
serde_json::from_value(serialized.clone()).expect("deserialize exec params");
assert_eq!(round_trip, params);
serialized
.as_object_mut()
.expect("exec params object")
.remove("managedNetwork");
let legacy: ExecParams =
serde_json::from_value(serialized).expect("deserialize legacy exec params");
assert!(legacy.enforce_managed_network);
assert_eq!(legacy.managed_network, None);
}
#[test]
fn environment_info_accepts_legacy_response_without_cwd() {
let info: EnvironmentInfo = serde_json::from_value(serde_json::json!({
"shell": { "name": "zsh", "path": "/bin/zsh" }
}))
.expect("legacy environment info should deserialize");
assert_eq!(
info,
EnvironmentInfo {
shell: ShellInfo {
name: "zsh".to_string(),
path: "/bin/zsh".to_string(),
},
cwd: None,
}
);
}
#[test]
fn filesystem_protocol_rejects_native_absolute_paths() {
let native_path = std::env::current_dir()
.expect("current directory")
.join("native-file.txt");
let native_cwd = std::env::current_dir().expect("current directory");
serde_json::from_value::<FsReadFileParams>(serde_json::json!({
"path": native_path.to_string_lossy(),
"sandbox": null,
}))
.expect_err("native absolute path should not deserialize as a URI");
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
PermissionProfile::default(),
PathUri::from_host_native_path(&native_cwd).expect("cwd URI"),
);
let mut native_path_sandbox =
serde_json::to_value(sandbox).expect("sandbox should serialize");
native_path_sandbox["cwd"] = serde_json::json!(native_cwd.to_string_lossy());
serde_json::from_value::<FsReadFileParams>(serde_json::json!({
"path": PathUri::from_host_native_path(native_path)
.expect("path URI")
.to_string(),
"sandbox": native_path_sandbox,
}))
.expect_err("native absolute sandbox cwd should not deserialize as a URI");
}
#[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))
);
}
}
+3 -4
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCMessage;
use futures::Sink;
use futures::SinkExt;
use futures::Stream;
@@ -847,8 +847,8 @@ mod tests {
use std::task::Poll;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCRequest;
use codex_exec_server_protocol::RequestId;
use futures::Sink;
use futures::Stream;
use futures::channel::mpsc as futures_mpsc;
@@ -1101,7 +1101,6 @@ mod tests {
id: RequestId::Integer(1),
method: "test".to_string(),
params: None,
trace: None,
})
}
@@ -1,7 +1,7 @@
#![allow(clippy::expect_used)]
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
+9 -10
View File
@@ -6,13 +6,13 @@ use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCError;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCRequest;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::RequestId;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
@@ -362,7 +362,6 @@ impl RpcClient {
id: request_id.clone(),
method: method.to_string(),
params: Some(params),
trace: None,
}))
.await
.is_err()
@@ -552,8 +551,8 @@ async fn drain_pending(pending: &Mutex<HashMap<RequestId, PendingRequest>>) {
mod tests {
use std::time::Duration;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCResponse;
use pretty_assertions::assert_eq;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;
@@ -1,6 +1,6 @@
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_utils_path_uri::PathUri;
use tokio::io;
@@ -2,7 +2,7 @@ use std::io;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
+2 -2
View File
@@ -3,8 +3,8 @@ use std::sync::Mutex as StdMutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::RequestId;
use serde_json::to_value;
use std::collections::HashSet;
use tokio::sync::Mutex;
@@ -1,4 +1,4 @@
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use crate::ExecServerRuntimePaths;
use crate::local_process::LocalProcess;
+10 -11
View File
@@ -89,7 +89,7 @@ async fn run_connection(
warn!("ignoring malformed exec-server message: {reason}");
if outgoing_tx
.send(RpcServerOutboundMessage::Error {
request_id: codex_app_server_protocol::RequestId::Integer(-1),
request_id: codex_exec_server_protocol::RequestId::Integer(-1),
error: invalid_request(reason),
})
.await
@@ -99,7 +99,7 @@ async fn run_connection(
}
}
JsonRpcConnectionEvent::Message(message) => match message {
codex_app_server_protocol::JSONRPCMessage::Request(request) => {
codex_exec_server_protocol::JSONRPCMessage::Request(request) => {
if let Some(route) = router.request_route(request.method.as_str()) {
let message = tokio::select! {
message = route(Arc::clone(&handler), request) => message,
@@ -127,7 +127,7 @@ async fn run_connection(
break;
}
}
codex_app_server_protocol::JSONRPCMessage::Notification(notification) => {
codex_exec_server_protocol::JSONRPCMessage::Notification(notification) => {
let Some(route) = router.notification_route(notification.method.as_str())
else {
warn!(
@@ -150,14 +150,14 @@ async fn run_connection(
break;
}
}
codex_app_server_protocol::JSONRPCMessage::Response(response) => {
codex_exec_server_protocol::JSONRPCMessage::Response(response) => {
warn!(
"closing exec-server connection after unexpected client response: {:?}",
response.id
);
break;
}
codex_app_server_protocol::JSONRPCMessage::Error(error) => {
codex_exec_server_protocol::JSONRPCMessage::Error(error) => {
warn!(
"closing exec-server connection after unexpected client error: {:?}",
error.id
@@ -190,11 +190,11 @@ mod tests {
use std::sync::Arc;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCRequest;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::RequestId;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use serde::Serialize;
@@ -382,7 +382,6 @@ mod tests {
id: RequestId::Integer(id),
method: method.to_string(),
params: Some(serde_json::to_value(params).expect("serialize params")),
trace: None,
}),
)
.await;
@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::JSONRPCErrorError;
use tokio::sync::Mutex;
use uuid::Uuid;
@@ -1,11 +1,11 @@
use std::net::SocketAddr;
use std::time::Duration;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCNotification;
use codex_exec_server_protocol::JSONRPCRequest;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::RequestId;
use pretty_assertions::assert_eq;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;
@@ -74,7 +74,6 @@ async fn stdio_listen_transport_serves_initialize() {
})
.expect("initialize params should serialize"),
),
trace: None,
});
write_jsonrpc_line(&mut client_writer, &initialize).await;