| 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"; |
| |
| pub const CAPABILITY_ROOTS_DISCOVER_METHOD: &str = "capabilityRoots/discoverV1"; |
| |
| pub const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] = &[ |
| ".codex-plugin/plugin.json", |
| ".claude-plugin/plugin.json", |
| ".cursor-plugin/plugin.json", |
| ]; |
| |
| pub const HTTP_REQUEST_METHOD: &str = "http/request"; |
| |
| pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta"; |
| |
| pub const MAX_HTTP_BODY_DELTA_BYTES: usize = 1024 * 1024; |
|
|
| #[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, |
| |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub environment_info: Option<EnvironmentInfo>, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct EnvironmentInfo { |
| pub shell: ShellInfo, |
| |
| |
| #[serde(default = "unknown_executor_version")] |
| pub executor_version: String, |
| |
| |
| |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub provider_id: Option<String>, |
| |
| #[serde(default)] |
| pub cwd: Option<PathUri>, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub user_home_dir: Option<PathUri>, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub platform_os: Option<String>, |
| |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub temporary_directories: Option<Vec<PathUri>>, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub temp_dir: Option<PathUri>, |
| |
| #[serde(default)] |
| pub capabilities: EnvironmentCapabilities, |
| } |
|
|
| fn unknown_executor_version() -> String { |
| "0.0.0".to_string() |
| } |
|
|
| |
| #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct EnvironmentCapabilities { |
| |
| #[serde(default)] |
| pub network_proxy_launch: bool, |
| |
| #[serde(default)] |
| pub capability_discovery_sandbox: bool, |
| |
| #[serde(default)] |
| pub environment_config_read: bool, |
| |
| #[serde(default)] |
| pub http_header_env_vars: bool, |
| |
| #[serde(default)] |
| pub sandboxed_file_streaming: bool, |
| |
| #[serde(default)] |
| pub shell_snapshot_v2: bool, |
| |
| #[serde(default)] |
| pub windows_mxc: bool, |
| } |
|
|
| |
| |
| |
| |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct EnvironmentStatus { |
| pub status: EnvironmentStatusKind, |
| } |
|
|
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub enum EnvironmentStatusKind { |
| |
| Ready, |
| } |
|
|
| impl EnvironmentInfo { |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| pub fn local() -> Self { |
| #[cfg(windows)] |
| let windows_mxc = codex_mxc_sandbox::is_available(); |
| #[cfg(not(windows))] |
| 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, |
| }, |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ShellInfo { |
| |
| pub name: String, |
| |
| |
| 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(), |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ExecMetadata { |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub thread_id: Option<ThreadId>, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub tool_call_id: Option<String>, |
| } |
|
|
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ExecParams { |
| |
| |
| pub process_id: ProcessId, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub metadata: Option<ExecMetadata>, |
| pub argv: Vec<String>, |
| |
| pub cwd: PathUri, |
| #[serde(default)] |
| pub env_policy: Option<ExecEnvPolicy>, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub shell_snapshot: Option<ShellSnapshotRequest>, |
| pub env: HashMap<String, String>, |
| pub tty: bool, |
| |
| #[serde(default)] |
| pub pipe_stdin: bool, |
| |
| |
| pub arg0: Option<String>, |
| |
| #[serde(default)] |
| pub sandbox: Option<FileSystemSandboxContext>, |
| |
| #[serde(default)] |
| pub enforce_managed_network: bool, |
| |
| |
| |
| |
| #[serde(default)] |
| pub managed_network: Option<ManagedNetworkSandboxContext>, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub network_proxy: Option<RemoteNetworkProxyLaunchConfig>, |
| } |
|
|
| #[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 ShellSnapshotRequest { |
| |
| pub scope_id: String, |
| |
| pub shell: ShellInfo, |
| } |
|
|
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ExecResponse { |
| pub process_id: ProcessId, |
| |
| |
| #[serde(default)] |
| pub sandbox_type: Option<ProcessSandboxType>, |
| } |
|
|
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub enum ProcessSandboxType { |
| |
| None, |
| MacosSeatbelt, |
| LinuxSeccomp, |
| WindowsRestrictedToken, |
| WindowsMxc, |
| } |
|
|
| #[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>, |
| |
| #[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, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub follow_symlinks: Option<bool>, |
| 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, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub follow_symlinks: Option<bool>, |
| 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>, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub follow_symlinks: 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, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub follow_symlinks: Option<bool>, |
| 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 FsWalkParams { |
| pub path: PathUri, |
| pub options: WalkOptions, |
| pub sandbox: Option<FileSystemSandboxContext>, |
| } |
|
|
| pub type FsWalkResponse = WalkOutcome; |
|
|
| #[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>, |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub follow_symlinks: 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 {} |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct CapabilityRootsDiscoverParams { |
| pub roots: Vec<CapabilityRootDiscoverRequest>, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct CapabilityRootDiscoverRequest { |
| |
| pub id: String, |
| |
| pub path: PathUri, |
| |
| #[serde(default)] |
| pub sandbox: Option<FileSystemSandboxContext>, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct CapabilityRootsDiscoverResponse { |
| pub roots: Vec<CapabilityRootDiscovery>, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct CapabilityTextFile { |
| pub path: PathUri, |
| pub contents: String, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct DiscoveredPluginFiles { |
| pub manifest: CapabilityTextFile, |
| |
| #[serde(default)] |
| pub mcp_config: Option<CapabilityTextFile>, |
| |
| #[serde(default)] |
| pub apps_config: Option<CapabilityTextFile>, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct DiscoveredSkillFiles { |
| pub instructions: CapabilityTextFile, |
| #[serde(default)] |
| pub metadata: Option<CapabilityTextFile>, |
| } |
|
|
| |
| |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct CapabilityRootDiscovery { |
| pub id: String, |
| pub path: PathUri, |
| #[serde(default)] |
| pub plugin: Option<DiscoveredPluginFiles>, |
| #[serde(default)] |
| pub skills: Vec<DiscoveredSkillFiles>, |
| |
| #[serde(default)] |
| pub namespace_manifests: Vec<CapabilityTextFile>, |
| #[serde(default)] |
| pub warnings: Vec<String>, |
| #[serde(default)] |
| pub error: Option<String>, |
| } |
|
|
| |
| #[derive(Clone, Debug)] |
| pub struct ExecutorCapabilityDiscoverySnapshot { |
| roots: Arc<[ExecutorCapabilityDiscoverySnapshotEntry]>, |
| sandbox_contexts: Arc<HashMap<String, FileSystemSandboxContext>>, |
| } |
|
|
| #[derive(Clone, Debug)] |
| 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() |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct HttpHeader { |
| |
| pub name: String, |
| |
| pub value: String, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub value_env_var: Option<String>, |
| } |
|
|
| |
| #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub enum HttpRedirectPolicy { |
| |
| #[default] |
| Follow, |
| |
| Stop, |
| } |
|
|
| |
| |
| |
| |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct HttpRequestParams { |
| |
| pub method: String, |
| |
| pub url: String, |
| |
| #[serde(default)] |
| pub headers: Vec<HttpHeader>, |
| |
| #[serde(default, rename = "bodyBase64")] |
| pub body: Option<ByteChunk>, |
| |
| |
| |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub timeout_ms: Option<u64>, |
| |
| #[serde(default)] |
| pub redirect_policy: HttpRedirectPolicy, |
| |
| |
| |
| |
| |
| |
| pub request_id: String, |
| |
| #[serde(default)] |
| pub stream_response: bool, |
| } |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct HttpRequestResponse { |
| |
| pub status: u16, |
| |
| pub headers: Vec<HttpHeader>, |
| |
| #[serde(rename = "bodyBase64")] |
| pub body: ByteChunk, |
| } |
|
|
| |
| |
| |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct HttpRequestBodyDeltaNotification { |
| |
| pub request_id: String, |
| |
| pub seq: u64, |
| |
| #[serde(rename = "deltaBase64")] |
| pub delta: ByteChunk, |
| |
| #[serde(default)] |
| pub done: bool, |
| |
| #[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, |
| #[serde(default)] |
| pub sandbox_denied: Option<bool>, |
| } |
|
|
| #[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::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; |
|
|
| #[test] |
| 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()); |
| } |
|
|
| #[test] |
| 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, |
| } |
| ); |
| } |
|
|
| #[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(), |
| }, |
| 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(), |
| } |
| ); |
| } |
|
|
| #[test] |
| 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, |
| } |
| ); |
| } |
|
|
| #[test] |
| 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, |
| ); |
| } |
|
|
| #[test] |
| 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()); |
| } |
|
|
| #[cfg(unix)] |
| #[test] |
| 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)); |
| } |
|
|
| #[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 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 |
| ); |
| } |
|
|
| #[test] |
| 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 |
| ); |
| } |
|
|
| #[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)) |
| ); |
| } |
|
|
| #[test] |
| 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); |
| } |
|
|
| #[test] |
| 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)) |
| ); |
| } |
| } |
|
|