Download codex-rs/exec-server-protocol/src/protocol.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 53.6 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server-protocol/src/protocol.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/exec-server-protocol/src/protocol.rs
-
curl -L -o protocol.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server-protocol/src/protocol.rs
53.6 kB
| use std::collections::HashMap; | |
| use std::sync::Arc; | |
| use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; | |
| use codex_file_system::FileSystemSandboxContext; | |
| pub use codex_file_system::WalkOptions; | |
| pub use codex_file_system::WalkOutcome; | |
| use codex_network_proxy::ManagedNetworkSandboxContext; | |
| use codex_network_proxy::RemoteNetworkProxyLaunchConfig; | |
| use codex_protocol::ThreadId; | |
| use codex_protocol::capabilities::SelectedCapabilityRoot; | |
| use codex_protocol::config_types::ShellEnvironmentPolicyInherit; | |
| use codex_shell_command::shell_detect::DetectedShell; | |
| 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 ENVIRONMENT_STATUS_METHOD: &str = "environment/status"; | |
| pub const FS_READ_FILE_METHOD: &str = "fs/readFile"; | |
| pub const FS_OPEN_METHOD: &str = "fs/open"; | |
| pub const FS_READ_BLOCK_METHOD: &str = "fs/readBlock"; | |
| pub 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_WALK_METHOD: &str = "fs/walk"; | |
| pub const FS_REMOVE_METHOD: &str = "fs/remove"; | |
| pub const FS_COPY_METHOD: &str = "fs/copy"; | |
| /// Discovers capability manifests below selected roots using executor-local filesystem access. | |
| pub const CAPABILITY_ROOTS_DISCOVER_METHOD: &str = "capabilityRoots/discoverV1"; | |
| /// Ordered plugin manifest paths recognized beneath a plugin root. | |
| pub const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] = &[ | |
| ".codex-plugin/plugin.json", | |
| ".claude-plugin/plugin.json", | |
| ".cursor-plugin/plugin.json", | |
| ]; | |
| /// 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"; | |
| /// Maximum decoded response-body bytes carried by one streamed HTTP notification. | |
| pub const MAX_HTTP_BODY_DELTA_BYTES: usize = 1024 * 1024; | |
| pub struct ByteChunk( 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) | |
| } | |
| } | |
| pub struct InitializeParams { | |
| pub client_name: String, | |
| pub resume_session_id: Option<String>, | |
| } | |
| pub struct InitializeResponse { | |
| pub session_id: String, | |
| /// Executor metadata at initialization, with the same shape as `environment/info`. | |
| // TODO: Make this required once all supported exec-server versions return environmentInfo. | |
| pub environment_info: Option<EnvironmentInfo>, | |
| } | |
| /// Information about an execution/filesystem environment. | |
| pub struct EnvironmentInfo { | |
| pub shell: ShellInfo, | |
| /// Executor release version for version-based compatibility decisions. | |
| /// `0.0.0` when unknown, including responses from legacy executors. | |
| pub executor_version: String, | |
| /// Opaque executor build identity for looking up behavioral verification. | |
| /// Derived from the compiled commit and target for standard builds; | |
| /// absent for legacy or unstamped builds. This is not an artifact checksum | |
| /// or a security attestation, and evidence must not be shared across build variants. | |
| pub provider_id: Option<String>, | |
| /// Working directory inherited by the exec-server process. | |
| pub cwd: Option<PathUri>, | |
| /// Executor user home used to expand `~` in path-bearing values. | |
| pub user_home_dir: Option<PathUri>, | |
| /// Operating system reported by the executor; absent for legacy exec-servers. | |
| pub platform_os: Option<String>, | |
| /// Executor-local default directories for resolving `:tmpdir`, when reported. | |
| /// On Windows, a command's `TEMP` or `TMP` overrides take precedence. | |
| pub temporary_directories: Option<Vec<PathUri>>, | |
| /// Executor-native temporary directory for child-visible sidecars. | |
| pub temp_dir: Option<PathUri>, | |
| /// Optional executor features that clients must gate before sending newer request fields. | |
| pub capabilities: EnvironmentCapabilities, | |
| } | |
| fn unknown_executor_version() -> String { | |
| "0.0.0".to_string() | |
| } | |
| /// Features supported by the selected exec-server environment. | |
| pub struct EnvironmentCapabilities { | |
| /// Whether `exec` accepts instructions for launching an executor-local network proxy. | |
| pub network_proxy_launch: bool, | |
| /// Whether capability discovery applies the filesystem sandbox sent with each root. | |
| pub capability_discovery_sandbox: bool, | |
| /// Whether this executor supports the `environmentConfig/read` request. | |
| pub environment_config_read: bool, | |
| /// Whether HTTP headers can resolve values from the executor environment. | |
| pub http_header_env_vars: bool, | |
| /// Whether filesystem streams can use the requested platform sandbox. | |
| pub sandboxed_file_streaming: bool, | |
| /// Whether shell state can be cached and restored entirely inside the executor. | |
| pub shell_snapshot_v2: bool, | |
| /// Whether requests may explicitly select the MXC Windows sandbox backend. | |
| pub windows_mxc: bool, | |
| } | |
| /// Status returned by an initialized exec-server connection. | |
| /// | |
| /// The response is intentionally small today. New status details can be added | |
| /// without changing the method used by clients to verify that an initialized | |
| /// exec-server connection is still responsive. | |
| pub struct EnvironmentStatus { | |
| pub status: EnvironmentStatusKind, | |
| } | |
| /// High-level status reported by exec-server itself. | |
| pub enum EnvironmentStatusKind { | |
| /// The connection is initialized and exec-server can handle requests. | |
| Ready, | |
| } | |
| impl EnvironmentInfo { | |
| /// Returns executor-local default directories used to resolve `:tmpdir`. | |
| /// | |
| /// This is separate from `local` so orchestrator startup can cache the | |
| /// directories without repeating local shell detection. | |
| pub fn local_temporary_directories() -> Vec<PathUri> { | |
| let cwd = std::env::current_dir().ok(); | |
| Self::local_temporary_directories_with_cwd(cwd.as_deref()) | |
| } | |
| fn local_temporary_directories_with_cwd(cwd: Option<&std::path::Path>) -> Vec<PathUri> { | |
| let temporary_directory_env_vars: &[&str] = if cfg!(windows) { | |
| &["TEMP", "TMP"] | |
| } else { | |
| &["TMPDIR"] | |
| }; | |
| let normalize_temp_path = |path: std::ffi::OsString| { | |
| PathUri::from_host_native_path(&path).ok().or_else(|| { | |
| if cfg!(unix) { | |
| PathUri::from_host_native_path(cwd.as_ref()?.join(path)).ok() | |
| } else { | |
| None | |
| } | |
| }) | |
| }; | |
| let mut temporary_directories = Vec::new(); | |
| for name in temporary_directory_env_vars { | |
| if let Some(path) = std::env::var_os(name) | |
| .filter(|path| !path.is_empty()) | |
| .filter(|path| cfg!(unix) || std::path::Path::new(path).is_absolute()) | |
| .and_then(&normalize_temp_path) | |
| && !temporary_directories.contains(&path) | |
| { | |
| temporary_directories.push(path); | |
| } | |
| } | |
| temporary_directories | |
| } | |
| /// Returns information about the current local exec-server process. | |
| pub fn local() -> Self { | |
| let windows_mxc = codex_mxc_sandbox::is_available(); | |
| let windows_mxc = false; | |
| let cwd = std::env::current_dir().ok(); | |
| let temporary_directories = Self::local_temporary_directories_with_cwd(cwd.as_deref()); | |
| let normalize_temp_path = |path: std::ffi::OsString| { | |
| PathUri::from_host_native_path(&path).ok().or_else(|| { | |
| if cfg!(unix) { | |
| PathUri::from_host_native_path(cwd.as_ref()?.join(path)).ok() | |
| } else { | |
| None | |
| } | |
| }) | |
| }; | |
| let temp_dir = normalize_temp_path(std::env::temp_dir().into_os_string()); | |
| Self { | |
| shell: codex_shell_command::shell_detect::default_user_shell().into(), | |
| executor_version: unknown_executor_version(), | |
| provider_id: None, | |
| cwd: cwd.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()), | |
| user_home_dir: PathUri::from_host_native_path("~").ok(), | |
| platform_os: Some(std::env::consts::OS.to_string()), | |
| temporary_directories: Some(temporary_directories), | |
| temp_dir, | |
| capabilities: EnvironmentCapabilities { | |
| network_proxy_launch: true, | |
| capability_discovery_sandbox: true, | |
| environment_config_read: true, | |
| http_header_env_vars: true, | |
| sandboxed_file_streaming: true, | |
| shell_snapshot_v2: cfg!(unix), | |
| windows_mxc, | |
| }, | |
| } | |
| } | |
| } | |
| /// Shell detected for an execution/filesystem environment. | |
| 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, | |
| } | |
| 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(), | |
| } | |
| } | |
| } | |
| /// Optional tool attribution for executor telemetry, not authorization. | |
| pub struct ExecMetadata { | |
| pub thread_id: Option<ThreadId>, | |
| pub tool_call_id: Option<String>, | |
| } | |
| 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, | |
| /// Optional attribution; older clients omit it and older executors ignore it. | |
| pub metadata: Option<ExecMetadata>, | |
| pub argv: Vec<String>, | |
| /// Working directory URI, interpreted using the exec-server host's path rules at launch time. | |
| pub cwd: PathUri, | |
| pub env_policy: Option<ExecEnvPolicy>, | |
| /// Optional request to restore executor-owned, attachment-scoped shell state. | |
| pub shell_snapshot: Option<ShellSnapshotRequest>, | |
| pub env: HashMap<String, String>, | |
| pub tty: bool, | |
| /// Keep non-tty stdin writable through `process/write`. | |
| 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. | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| /// Whether the eventual executor-side sandbox must enforce managed networking. | |
| 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. | |
| pub managed_network: Option<ManagedNetworkSandboxContext>, | |
| /// Optional instructions for starting an executor-local managed-network proxy. | |
| pub network_proxy: Option<RemoteNetworkProxyLaunchConfig>, | |
| } | |
| 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>, | |
| } | |
| /// Identifies shell state owned by one attachment within an executor session. | |
| pub struct ShellSnapshotRequest { | |
| /// Attachment identity; executor sessions independently scope every cache. | |
| pub scope_id: String, | |
| /// Executor-native shell used to capture and restore the snapshot. | |
| pub shell: ShellInfo, | |
| } | |
| pub struct ExecResponse { | |
| pub process_id: ProcessId, | |
| /// `None` means the peer did not report its sandbox type. Current peers | |
| /// report [`ProcessSandboxType::None`] when the process was not sandboxed. | |
| pub sandbox_type: Option<ProcessSandboxType>, | |
| } | |
| pub enum ProcessSandboxType { | |
| /// The process was explicitly started without a platform sandbox. | |
| None, | |
| MacosSeatbelt, | |
| LinuxSeccomp, | |
| WindowsRestrictedToken, | |
| WindowsMxc, | |
| } | |
| pub struct ReadParams { | |
| pub process_id: ProcessId, | |
| pub after_seq: Option<u64>, | |
| pub max_bytes: Option<usize>, | |
| pub wait_ms: Option<u64>, | |
| } | |
| pub struct ProcessOutputChunk { | |
| pub seq: u64, | |
| pub stream: ExecOutputStream, | |
| pub chunk: ByteChunk, | |
| } | |
| 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. | |
| pub sandbox_denied: bool, | |
| } | |
| pub struct WriteParams { | |
| pub process_id: ProcessId, | |
| pub chunk: ByteChunk, | |
| pub write_id: String, | |
| } | |
| pub enum WriteStatus { | |
| Accepted, | |
| UnknownProcess, | |
| StdinClosed, | |
| Starting, | |
| } | |
| pub struct WriteResponse { | |
| pub status: WriteStatus, | |
| } | |
| pub enum ProcessSignal { | |
| Interrupt, | |
| } | |
| pub struct SignalParams { | |
| pub process_id: ProcessId, | |
| pub signal: ProcessSignal, | |
| } | |
| pub struct SignalResponse {} | |
| pub struct TerminateParams { | |
| pub process_id: ProcessId, | |
| } | |
| pub struct TerminateResponse { | |
| pub running: bool, | |
| } | |
| pub struct FsReadFileParams { | |
| pub path: PathUri, | |
| pub follow_symlinks: Option<bool>, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsReadFileResponse { | |
| pub data_base64: String, | |
| } | |
| pub struct FsOpenParams { | |
| pub handle_id: String, | |
| pub path: PathUri, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsOpenResponse { | |
| pub handle_id: String, | |
| } | |
| pub struct FsReadBlockParams { | |
| pub handle_id: String, | |
| pub offset: u64, | |
| pub len: usize, | |
| } | |
| pub struct FsReadBlockResponse { | |
| pub chunk: ByteChunk, | |
| pub eof: bool, | |
| } | |
| pub struct FsCloseParams { | |
| pub handle_id: String, | |
| } | |
| pub struct FsCloseResponse {} | |
| pub struct FsWriteFileParams { | |
| pub path: PathUri, | |
| pub data_base64: String, | |
| pub follow_symlinks: Option<bool>, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsWriteFileResponse {} | |
| pub struct FsCreateDirectoryParams { | |
| pub path: PathUri, | |
| pub recursive: Option<bool>, | |
| pub follow_symlinks: Option<bool>, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsCreateDirectoryResponse {} | |
| pub struct FsGetMetadataParams { | |
| pub path: PathUri, | |
| pub follow_symlinks: Option<bool>, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| 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, | |
| } | |
| pub struct FsCanonicalizeParams { | |
| pub path: PathUri, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsCanonicalizeResponse { | |
| pub path: PathUri, | |
| } | |
| pub struct FsReadDirectoryParams { | |
| pub path: PathUri, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsReadDirectoryEntry { | |
| pub file_name: String, | |
| pub is_directory: bool, | |
| pub is_file: bool, | |
| } | |
| pub struct FsReadDirectoryResponse { | |
| pub entries: Vec<FsReadDirectoryEntry>, | |
| } | |
| pub struct FsWalkParams { | |
| pub path: PathUri, | |
| pub options: WalkOptions, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub type FsWalkResponse = WalkOutcome; | |
| pub struct FsRemoveParams { | |
| pub path: PathUri, | |
| pub recursive: Option<bool>, | |
| pub force: Option<bool>, | |
| pub follow_symlinks: Option<bool>, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsRemoveResponse {} | |
| pub struct FsCopyParams { | |
| pub source_path: PathUri, | |
| pub destination_path: PathUri, | |
| pub recursive: bool, | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| pub struct FsCopyResponse {} | |
| /// Roots to inspect for plugin and skill capability manifests. | |
| pub struct CapabilityRootsDiscoverParams { | |
| pub roots: Vec<CapabilityRootDiscoverRequest>, | |
| } | |
| /// One caller-selected capability root. | |
| pub struct CapabilityRootDiscoverRequest { | |
| /// Opaque caller identity returned unchanged in the response. | |
| pub id: String, | |
| /// Absolute root URI interpreted using the exec-server host's path rules. | |
| pub path: PathUri, | |
| /// Filesystem permissions for this root and its symlink targets. | |
| pub sandbox: Option<FileSystemSandboxContext>, | |
| } | |
| /// Executor-local discovery results in request order. | |
| pub struct CapabilityRootsDiscoverResponse { | |
| pub roots: Vec<CapabilityRootDiscovery>, | |
| } | |
| /// Recognized UTF-8 capability file materialized by the exec-server. | |
| pub struct CapabilityTextFile { | |
| pub path: PathUri, | |
| pub contents: String, | |
| } | |
| /// Plugin files declared directly by a selected root. | |
| pub struct DiscoveredPluginFiles { | |
| pub manifest: CapabilityTextFile, | |
| /// File-backed MCP declarations, including the conventional `.mcp.json` fallback. | |
| pub mcp_config: Option<CapabilityTextFile>, | |
| /// File-backed connector declarations. | |
| pub apps_config: Option<CapabilityTextFile>, | |
| } | |
| /// A skill instructions file and its optional sibling metadata. | |
| pub struct DiscoveredSkillFiles { | |
| pub instructions: CapabilityTextFile, | |
| pub metadata: Option<CapabilityTextFile>, | |
| } | |
| /// Manifest bundle for one selected root. | |
| /// | |
| /// Discovery failures are root-local so one broken package does not discard valid siblings. | |
| pub struct CapabilityRootDiscovery { | |
| pub id: String, | |
| pub path: PathUri, | |
| pub plugin: Option<DiscoveredPluginFiles>, | |
| pub skills: Vec<DiscoveredSkillFiles>, | |
| /// Plugin manifests found while scanning the root, used to namespace nested skills. | |
| pub namespace_manifests: Vec<CapabilityTextFile>, | |
| pub warnings: Vec<String>, | |
| pub error: Option<String>, | |
| } | |
| /// Immutable results for the selected capability roots visible in one model step. | |
| pub struct ExecutorCapabilityDiscoverySnapshot { | |
| roots: Arc<[ExecutorCapabilityDiscoverySnapshotEntry]>, | |
| sandbox_contexts: Arc<HashMap<String, FileSystemSandboxContext>>, | |
| } | |
| pub struct ExecutorCapabilityDiscoverySnapshotEntry { | |
| pub selected_root: SelectedCapabilityRoot, | |
| pub result: Result<Arc<CapabilityRootDiscovery>, String>, | |
| } | |
| impl ExecutorCapabilityDiscoverySnapshot { | |
| pub fn new( | |
| selected_roots: &[SelectedCapabilityRoot], | |
| discoveries: Vec<Result<Arc<CapabilityRootDiscovery>, String>>, | |
| sandbox_contexts: HashMap<String, FileSystemSandboxContext>, | |
| ) -> Self { | |
| debug_assert_eq!(selected_roots.len(), discoveries.len()); | |
| Self { | |
| roots: selected_roots | |
| .iter() | |
| .cloned() | |
| .zip(discoveries) | |
| .map( | |
| |(selected_root, result)| ExecutorCapabilityDiscoverySnapshotEntry { | |
| selected_root, | |
| result, | |
| }, | |
| ) | |
| .collect(), | |
| sandbox_contexts: Arc::new(sandbox_contexts), | |
| } | |
| } | |
| pub fn roots(&self) -> &[ExecutorCapabilityDiscoverySnapshotEntry] { | |
| &self.roots | |
| } | |
| pub fn sandbox_contexts(&self) -> &HashMap<String, FileSystemSandboxContext> { | |
| self.sandbox_contexts.as_ref() | |
| } | |
| } | |
| /// HTTP header represented in the executor protocol. | |
| pub struct HttpHeader { | |
| /// Header name as it appears on the HTTP wire. | |
| pub name: String, | |
| /// Literal header value, or prefix for an executor-local environment value. | |
| pub value: String, | |
| /// Environment variable resolved by the process that sends the HTTP request. | |
| pub value_env_var: Option<String>, | |
| } | |
| /// Redirect behavior for an executor-side HTTP request. | |
| pub enum HttpRedirectPolicy { | |
| /// Follow redirects using the HTTP client's normal limits. | |
| Follow, | |
| /// Return the redirect response without following its location. | |
| Stop, | |
| } | |
| /// 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. | |
| 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. | |
| pub headers: Vec<HttpHeader>, | |
| /// Optional request body bytes. | |
| pub body: Option<ByteChunk>, | |
| /// Request timeout in milliseconds. | |
| /// | |
| /// Omitted or `null` disables the timeout. A number applies that exact | |
| /// millisecond deadline. | |
| pub timeout_ms: Option<u64>, | |
| /// Whether the executor should follow HTTP redirects. | |
| pub redirect_policy: HttpRedirectPolicy, | |
| /// 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. | |
| pub stream_response: bool, | |
| } | |
| /// HTTP response envelope returned from an executor `http/request` call. | |
| 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. | |
| 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. | |
| 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. | |
| pub delta: ByteChunk, | |
| /// Marks response-body EOF. No later deltas are expected for this request. | |
| pub done: bool, | |
| /// Terminal stream error. Set only on the final notification. | |
| pub error: Option<String>, | |
| } | |
| pub enum ExecOutputStream { | |
| Stdout, | |
| Stderr, | |
| Pty, | |
| } | |
| pub struct ExecOutputDeltaNotification { | |
| pub process_id: ProcessId, | |
| pub seq: u64, | |
| pub stream: ExecOutputStream, | |
| pub chunk: ByteChunk, | |
| } | |
| pub struct ExecExitedNotification { | |
| pub process_id: ProcessId, | |
| pub seq: u64, | |
| pub exit_code: i32, | |
| pub sandbox_denied: Option<bool>, | |
| } | |
| 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) | |
| } | |
| } | |
| mod tests { | |
| use super::EnvironmentCapabilities; | |
| use super::EnvironmentInfo; | |
| use super::ExecExitedNotification; | |
| use super::ExecMetadata; | |
| use super::ExecParams; | |
| use super::ExecResponse; | |
| use super::FsReadFileParams; | |
| use super::HttpRequestParams; | |
| use super::ProcessId; | |
| use super::ProcessSandboxType; | |
| use super::ShellInfo; | |
| use codex_file_system::FileSystemSandboxContext; | |
| use codex_file_system::WindowsSandboxSelection; | |
| use codex_network_proxy::ManagedNetworkSandboxContext; | |
| use codex_network_proxy::NetworkProxyAuditMetadata; | |
| use codex_network_proxy::NetworkProxyConfig; | |
| use codex_network_proxy::RemoteNetworkProxyConfig; | |
| use codex_network_proxy::RemoteNetworkProxyLaunchConfig; | |
| use codex_protocol::config_types::WindowsSandboxProxySettingsMode; | |
| use codex_protocol::models::ManagedFileSystemPermissions; | |
| use codex_protocol::models::PermissionProfile; | |
| use codex_protocol::permissions::FileSystemAccessMode; | |
| use codex_protocol::permissions::FileSystemPath; | |
| use codex_protocol::permissions::FileSystemSandboxEntry; | |
| use codex_protocol::permissions::FileSystemSandboxPolicy; | |
| use codex_protocol::permissions::FileSystemSpecialPath; | |
| use codex_protocol::permissions::NetworkSandboxPolicy; | |
| use codex_utils_path_uri::PathUri; | |
| use pretty_assertions::assert_eq; | |
| use std::collections::HashMap; | |
| fn exec_params_keeps_proxy_launch_separate_from_sandbox_facts() { | |
| 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"), | |
| metadata: Some(ExecMetadata { | |
| thread_id: Some(codex_protocol::ThreadId::new()), | |
| tool_call_id: Some("call-1".to_string()), | |
| }), | |
| argv: vec!["true".to_string()], | |
| cwd, | |
| env_policy: None, | |
| shell_snapshot: 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, | |
| allow_unix_sockets: vec!["/tmp/allowed.sock".to_string()], | |
| dangerously_allow_all_unix_sockets: true, | |
| }), | |
| network_proxy: Some( | |
| RemoteNetworkProxyLaunchConfig::new( | |
| RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default()) | |
| .expect("supported remote config"), | |
| ) | |
| .with_audit_metadata(NetworkProxyAuditMetadata { | |
| conversation_id: Some("conversation-1".to_string()), | |
| ..NetworkProxyAuditMetadata::default() | |
| }) | |
| .for_execution("remote".to_string(), "execution-1".to_string()), | |
| ), | |
| }; | |
| let mut serialized = serde_json::to_value(¶ms).expect("serialize exec params"); | |
| assert_eq!( | |
| ( | |
| serialized.get("threadId").cloned(), | |
| serialized.get("toolCallId").cloned(), | |
| serialized.get("metadata").cloned(), | |
| ), | |
| (None, None, Some(serde_json::json!(params.metadata)),) | |
| ); | |
| assert_eq!( | |
| serialized["managedNetwork"], | |
| serde_json::json!({ | |
| "loopbackPorts": [43123, 48081], | |
| "allowLocalBinding": false, | |
| "allowUnixSockets": ["/tmp/allowed.sock"], | |
| "dangerouslyAllowAllUnixSockets": true, | |
| }) | |
| ); | |
| assert_eq!( | |
| serialized["networkProxy"]["auditMetadata"]["conversationId"], | |
| "conversation-1" | |
| ); | |
| 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"); | |
| serialized | |
| .as_object_mut() | |
| .expect("exec params object") | |
| .remove("networkProxy"); | |
| serialized.as_object_mut().unwrap().remove("metadata"); | |
| let legacy: ExecParams = | |
| serde_json::from_value(serialized).expect("deserialize legacy exec params"); | |
| assert!(legacy.enforce_managed_network); | |
| assert_eq!(legacy.managed_network, None); | |
| assert_eq!(legacy.network_proxy, None); | |
| assert_eq!(legacy.metadata, None); | |
| let legacy_serialized = | |
| serde_json::to_value(&legacy).expect("serialize exec params without proxy launch"); | |
| assert!(legacy_serialized.get("networkProxy").is_none()); | |
| assert!(legacy_serialized.get("threadId").is_none()); | |
| assert!(legacy_serialized.get("toolCallId").is_none()); | |
| assert!(legacy_serialized.get("metadata").is_none()); | |
| } | |
| fn exec_params_defaults_legacy_managed_network_unix_socket_policy() { | |
| let cwd = | |
| PathUri::from_host_native_path(std::env::current_dir().expect("current directory")) | |
| .expect("cwd URI"); | |
| let legacy: ExecParams = serde_json::from_value(serde_json::json!({ | |
| "processId": "legacy-managed-network", | |
| "argv": ["true"], | |
| "cwd": cwd, | |
| "env": {}, | |
| "tty": false, | |
| "arg0": null, | |
| "enforceManagedNetwork": true, | |
| "managedNetwork": { | |
| "loopbackPorts": [43123], | |
| "allowLocalBinding": true, | |
| }, | |
| })) | |
| .expect("deserialize legacy managed network context"); | |
| assert_eq!( | |
| legacy, | |
| ExecParams { | |
| process_id: ProcessId::from("legacy-managed-network"), | |
| metadata: None, | |
| argv: vec!["true".to_string()], | |
| cwd, | |
| env_policy: None, | |
| shell_snapshot: 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], | |
| allow_local_binding: true, | |
| allow_unix_sockets: Vec::new(), | |
| dangerously_allow_all_unix_sockets: false, | |
| }), | |
| network_proxy: None, | |
| } | |
| ); | |
| } | |
| 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(), | |
| }, | |
| executor_version: "0.0.0".to_string(), | |
| provider_id: None, | |
| cwd: None, | |
| user_home_dir: None, | |
| platform_os: None, | |
| temporary_directories: None, | |
| temp_dir: None, | |
| capabilities: EnvironmentCapabilities::default(), | |
| } | |
| ); | |
| } | |
| fn environment_capabilities_accept_legacy_response_without_environment_config_read() { | |
| let capabilities: EnvironmentCapabilities = serde_json::from_value(serde_json::json!({ | |
| "networkProxyLaunch": true, | |
| "capabilityDiscoverySandbox": true, | |
| })) | |
| .expect("legacy environment capabilities should deserialize"); | |
| assert_eq!( | |
| capabilities, | |
| EnvironmentCapabilities { | |
| network_proxy_launch: true, | |
| capability_discovery_sandbox: true, | |
| environment_config_read: false, | |
| http_header_env_vars: false, | |
| sandboxed_file_streaming: false, | |
| shell_snapshot_v2: false, | |
| windows_mxc: false, | |
| } | |
| ); | |
| } | |
| fn environment_info_preserves_executor_metadata() { | |
| let expected = serde_json::json!({ | |
| "shell": { "name": "powershell", "path": "powershell.exe" }, | |
| "executorVersion": "1.2.3-alpha.4", | |
| "providerId": "sha256:e0a0cebe63ab8189ffe3eed378ccf6aa89ef15bc75e39dbbf1fc55951ec6888b", | |
| "cwd": null, | |
| "userHomeDir": "file:///C:/Users/remote", | |
| "platformOs": "windows", | |
| "temporaryDirectories": ["file:///C:/Temp", "file:///D:/Temp"], | |
| "capabilities": { | |
| "networkProxyLaunch": false, | |
| "capabilityDiscoverySandbox": false, | |
| "environmentConfigRead": false, | |
| "httpHeaderEnvVars": false, | |
| "sandboxedFileStreaming": false, | |
| "shellSnapshotV2": false, | |
| "windowsMxc": false, | |
| }, | |
| }); | |
| let info: EnvironmentInfo = serde_json::from_value(expected.clone()) | |
| .expect("environment info with executor metadata should deserialize"); | |
| assert_eq!( | |
| serde_json::to_value(info).expect("environment info should serialize"), | |
| expected, | |
| ); | |
| } | |
| fn local_environment_info_reads_platform_temporary_directories() { | |
| let cwd = std::env::current_dir().expect("current directory"); | |
| let names: &[&str] = if cfg!(windows) { | |
| &["TEMP", "TMP"] | |
| } else { | |
| &["TMPDIR"] | |
| }; | |
| let mut expected = names | |
| .iter() | |
| .filter_map(std::env::var_os) | |
| .filter(|path| !path.is_empty()) | |
| .filter(|path| cfg!(unix) || std::path::Path::new(path).is_absolute()) | |
| .filter_map(|path| { | |
| PathUri::from_host_native_path(&path).ok().or_else(|| { | |
| if cfg!(unix) { | |
| PathUri::from_host_native_path(cwd.join(path)).ok() | |
| } else { | |
| None | |
| } | |
| }) | |
| }) | |
| .collect::<Vec<_>>(); | |
| expected.dedup(); | |
| let info = EnvironmentInfo::local(); | |
| assert_eq!(info.temporary_directories, Some(expected)); | |
| assert_eq!(info.user_home_dir, PathUri::from_host_native_path("~").ok()); | |
| } | |
| fn local_environment_info_resolves_relative_temporary_directory() { | |
| if std::env::var_os("CODEX_TEST_RELATIVE_TMPDIR").is_none() { | |
| let status = std::process::Command::new(std::env::current_exe().expect("test binary")) | |
| .arg("--exact") | |
| .arg( | |
| "protocol::tests::local_environment_info_resolves_relative_temporary_directory", | |
| ) | |
| .env("CODEX_TEST_RELATIVE_TMPDIR", "1") | |
| .env("TMPDIR", "relative-temp") | |
| .status() | |
| .expect("run relative TMPDIR subprocess"); | |
| assert!(status.success(), "relative TMPDIR subprocess failed"); | |
| return; | |
| } | |
| let expected = PathUri::from_host_native_path( | |
| std::env::current_dir() | |
| .expect("current directory") | |
| .join("relative-temp"), | |
| ) | |
| .expect("absolute temporary directory URI"); | |
| let info = EnvironmentInfo::local(); | |
| assert_eq!(info.temporary_directories, Some(vec![expected.clone()])); | |
| assert_eq!(info.temp_dir, Some(expected)); | |
| } | |
| 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"); | |
| } | |
| fn filesystem_protocol_round_trips_permission_entries() { | |
| let native_cwd = std::env::current_dir().expect("current directory"); | |
| let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI"); | |
| let file_system = ManagedFileSystemPermissions::Restricted { | |
| entries: vec![ | |
| FileSystemSandboxEntry { | |
| path: FileSystemPath::Path { path: cwd.clone() }, | |
| access: FileSystemAccessMode::Read, | |
| missing_path_behavior: None, | |
| }, | |
| FileSystemSandboxEntry::skip_missing_path( | |
| FileSystemPath::Path { | |
| path: PathUri::from_host_native_path(native_cwd.join(".git")) | |
| .expect("absolute path"), | |
| }, | |
| FileSystemAccessMode::Read, | |
| ), | |
| FileSystemSandboxEntry::skip_missing_path( | |
| FileSystemPath::Special { | |
| value: FileSystemSpecialPath::ProjectRoots { | |
| subpath: Some(".codex".into()), | |
| }, | |
| }, | |
| FileSystemAccessMode::Read, | |
| ), | |
| ], | |
| glob_scan_max_depth: Some(2.try_into().expect("non-zero depth")), | |
| }; | |
| let permissions = PermissionProfile::Managed { | |
| file_system, | |
| network: NetworkSandboxPolicy::Restricted, | |
| }; | |
| let mut sandbox = | |
| FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone()); | |
| sandbox.user_home_dir = Some(cwd.clone()); | |
| sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc; | |
| let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox"); | |
| assert_eq!(serialized["windowsSandboxLevel"], "mxc"); | |
| assert_eq!( | |
| serialized["userHomeDir"], | |
| serde_json::json!(cwd.to_string()) | |
| ); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][0]["path"]["path"], | |
| serde_json::json!(cwd.to_string()) | |
| ); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][1]["path"]["type"], | |
| serde_json::json!("path") | |
| ); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][1]["missing_path_behavior"], | |
| serde_json::json!("skip") | |
| ); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][2]["path"]["type"], | |
| serde_json::json!("special") | |
| ); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][2]["missing_path_behavior"], | |
| serde_json::json!("skip") | |
| ); | |
| assert!(!serialized.to_string().contains("generated_default_path")); | |
| assert!(!serialized.to_string().contains("generated_default_special")); | |
| assert_eq!( | |
| serde_json::from_value::<FileSystemSandboxContext>(serialized) | |
| .expect("deserialize sandbox"), | |
| sandbox | |
| ); | |
| let preserve = FileSystemSandboxContext { | |
| windows_sandbox_proxy_settings_mode: Some(WindowsSandboxProxySettingsMode::Preserve), | |
| ..sandbox | |
| }; | |
| let serialized = serde_json::to_value(&preserve).expect("serialize preserve mode"); | |
| assert_eq!(serialized["windowsSandboxProxySettingsMode"], "preserve"); | |
| assert_eq!( | |
| serde_json::from_value::<FileSystemSandboxContext>(serialized) | |
| .expect("deserialize preserve mode"), | |
| preserve | |
| ); | |
| } | |
| fn filesystem_protocol_round_trips_legacy_policy_paths_as_uris() { | |
| let native_cwd = std::env::current_dir().expect("current directory"); | |
| let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI"); | |
| let mut file_system_policy = | |
| FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { | |
| path: FileSystemPath::Path { path: cwd.clone() }, | |
| access: FileSystemAccessMode::Read, | |
| missing_path_behavior: None, | |
| }]); | |
| file_system_policy.glob_scan_max_depth = Some(2); | |
| let permissions = PermissionProfile::from_runtime_permissions( | |
| &file_system_policy, | |
| NetworkSandboxPolicy::Restricted, | |
| ); | |
| let sandbox = | |
| FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone()); | |
| let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox"); | |
| assert_eq!( | |
| serialized["permissions"]["file_system"]["entries"][0]["path"]["path"], | |
| serde_json::json!(cwd.to_string()) | |
| ); | |
| assert_eq!( | |
| serde_json::from_value::<FileSystemSandboxContext>(serialized) | |
| .expect("deserialize sandbox"), | |
| sandbox | |
| ); | |
| } | |
| 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)) | |
| ); | |
| } | |
| fn exited_notification_accepts_legacy_payload_without_sandbox_denied() { | |
| let notification: ExecExitedNotification = serde_json::from_value(serde_json::json!({ | |
| "processId": "proc-1", | |
| "seq": 3, | |
| "exitCode": 1, | |
| })) | |
| .expect("legacy exited notification should deserialize"); | |
| assert_eq!(notification.sandbox_denied, None); | |
| } | |
| fn exec_response_distinguishes_unknown_from_explicitly_unsandboxed() { | |
| let unknown: ExecResponse = serde_json::from_value(serde_json::json!({ | |
| "processId": "legacy", | |
| })) | |
| .expect("legacy response should deserialize"); | |
| let unsandboxed: ExecResponse = serde_json::from_value(serde_json::json!({ | |
| "processId": "current", | |
| "sandboxType": "none", | |
| })) | |
| .expect("explicitly unsandboxed response should deserialize"); | |
| assert_eq!( | |
| (unknown.sandbox_type, unsandboxed.sandbox_type), | |
| (None, Some(ProcessSandboxType::None)) | |
| ); | |
| } | |
| } | |