diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 5990d8811d..4ac84f9663 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -22,6 +22,7 @@ use crate::environment_toml::environment_provider_from_codex_home; use crate::local_file_system::LocalFileSystem; use crate::local_process::LocalProcess; use crate::process::ExecBackend; +use crate::protocol::EnvironmentCapabilities; use crate::protocol::EnvironmentInfo; use crate::protocol::ShellInfo; use crate::remote::NoiseRendezvousEnvironmentConfig; @@ -608,6 +609,7 @@ impl EnvironmentInfo { pub(crate) fn local() -> Self { Self { shell: codex_shell_command::shell_detect::default_user_shell().into(), + capabilities: EnvironmentCapabilities::default(), } } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index d5c71674ac..6e3c5830ab 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -80,6 +80,7 @@ pub use process::ExecProcessFuture; pub use process::StartedExecProcess; pub use process_id::ProcessId; pub use protocol::ByteChunk; +pub use protocol::EnvironmentCapabilities; pub use protocol::EnvironmentInfo; pub use protocol::ExecClosedNotification; pub use protocol::ExecEnvPolicy; @@ -88,6 +89,20 @@ pub use protocol::ExecOutputDeltaNotification; pub use protocol::ExecOutputStream; pub use protocol::ExecParams; pub use protocol::ExecResponse; +pub use protocol::FileTransferCancelParams; +pub use protocol::FileTransferCancelResponse; +pub use protocol::FileTransferDigest; +pub use protocol::FileTransferDigestAlgorithm; +pub use protocol::FileTransferHeader; +pub use protocol::FileTransferOperationState; +pub use protocol::FileTransferPrepareUploadParams; +pub use protocol::FileTransferPrepareUploadResponse; +pub use protocol::FileTransferStartUploadParams; +pub use protocol::FileTransferStartUploadResponse; +pub use protocol::FileTransferStatusParams; +pub use protocol::FileTransferStatusResponse; +pub use protocol::FileTransferUploadDescriptor; +pub use protocol::FileTransferUploadDescriptorKind; pub use protocol::FsCanonicalizeParams; pub use protocol::FsCanonicalizeResponse; pub use protocol::FsCloseParams; @@ -117,6 +132,9 @@ pub use protocol::HttpRequestParams; pub use protocol::HttpRequestResponse; pub use protocol::InitializeParams; pub use protocol::InitializeResponse; +pub use protocol::MAX_PREPARED_FILE_UPLOAD_BYTES; +pub use protocol::PREPARED_FILE_UPLOAD_PROTOCOL_VERSION; +pub use protocol::PreparedFileUploadCapability; pub use protocol::ProcessOutputChunk; pub use protocol::ProcessSignal; pub use protocol::ReadParams; diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 9c00f8f465..31dde7cf49 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -71,6 +71,166 @@ pub struct InitializeResponse { #[serde(rename_all = "camelCase")] pub struct EnvironmentInfo { pub shell: ShellInfo, + #[serde(default)] + pub capabilities: EnvironmentCapabilities, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnvironmentCapabilities { + /// Executor-owned upload support. This primitive is intended for trusted + /// orchestration code that performs approval before preparing a source and + /// again after presenting the exact name, size, and digest to the user. + #[serde(default)] + pub prepared_file_upload: Option, +} + +pub const PREPARED_FILE_UPLOAD_PROTOCOL_VERSION: u32 = 1; +pub const MAX_PREPARED_FILE_UPLOAD_BYTES: u64 = 8 * 1024 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreparedFileUploadCapability { + pub protocol_version: u32, + pub max_upload_bytes: u64, + pub descriptor_kinds: Vec, + pub supports_status_reconciliation: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum FileTransferUploadDescriptorKind { + HttpsPut, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum FileTransferDigestAlgorithm { + Sha256, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferDigest { + pub algorithm: FileTransferDigestAlgorithm, + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferPrepareUploadParams { + pub path: PathUri, + /// Authoritative permission context selected by the trusted caller. This + /// may intentionally be full access after the caller's read approval. + pub sandbox: FileSystemSandboxContext, + pub max_bytes: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferPrepareUploadResponse { + /// Opaque, session-bound identifier used for the entire lifecycle. + pub transfer_id: String, + pub name: String, + pub size: u64, + pub digest: FileTransferDigest, + /// The snapshot's upper-bound wall-clock expiry. The session may expire + /// sooner when its transport remains detached. + pub expires_at_unix_seconds: i64, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferHeader { + pub name: String, + pub value: String, +} + +impl std::fmt::Debug for FileTransferHeader { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("FileTransferHeader") + .field("name", &self.name) + .field("value", &"[REDACTED]") + .finish() + } +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum FileTransferUploadDescriptor { + HttpsPut { + url: String, + #[serde(default)] + headers: Vec, + expires_at_unix_seconds: i64, + }, +} + +impl std::fmt::Debug for FileTransferUploadDescriptor { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HttpsPut { headers, .. } => formatter + .debug_struct("HttpsPut") + .field("url", &"[REDACTED]") + .field("header_count", &headers.len()) + .field("expires_at_unix_seconds", &"[REDACTED]") + .finish(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferStartUploadParams { + pub transfer_id: String, + pub descriptor: FileTransferUploadDescriptor, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferStartUploadResponse { + pub transfer_id: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum FileTransferOperationState { + Prepared, + Uploading, + CancelRequested, + Succeeded, + Failed, + Canceled, + CompletionUnknown, + Expired, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferStatusParams { + pub transfer_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferStatusResponse { + pub transfer_id: String, + pub state: FileTransferOperationState, + #[serde(default)] + pub error: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferCancelParams { + pub transfer_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileTransferCancelResponse { + pub state: FileTransferOperationState, } /// Shell detected for an execution/filesystem environment. @@ -503,6 +663,10 @@ mod base64_bytes { #[cfg(test)] mod tests { + use super::EnvironmentCapabilities; + use super::EnvironmentInfo; + use super::FileTransferHeader; + use super::FileTransferUploadDescriptor; use super::FsReadFileParams; use super::HttpRequestParams; use codex_file_system::FileSystemSandboxContext; @@ -510,6 +674,33 @@ mod tests { use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; + #[test] + fn environment_info_accepts_legacy_payload_without_capabilities() { + let info: EnvironmentInfo = serde_json::from_value(serde_json::json!({ + "shell": {"name": "sh", "path": "/bin/sh"}, + })) + .expect("legacy environment info should deserialize"); + + assert_eq!(info.capabilities, EnvironmentCapabilities::default()); + } + + #[test] + fn upload_descriptor_debug_redacts_secrets() { + let descriptor = FileTransferUploadDescriptor::HttpsPut { + url: "https://account.blob.core.windows.net/object?sig=secret".to_string(), + headers: vec![FileTransferHeader { + name: "x-ms-meta-token".to_string(), + value: "secret-header".to_string(), + }], + expires_at_unix_seconds: 123, + }; + + let debug = format!("{descriptor:?}"); + assert!(!debug.contains("sig=secret")); + assert!(!debug.contains("secret-header")); + assert!(debug.contains("header_count")); + } + #[test] fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris() { let legacy_path = std::env::current_dir()