diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 8d94c15955..a34dd61dd4 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -497,6 +497,24 @@ pub struct ShellSnapshotRequest { pub shell: ShellInfo, } +/// Optional JSON-RPC error data for a rejected `process/start`. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum ProcessStartErrorData { + SpawnNotFound, + SpawnPermissionDenied, + SpawnFailed, + SandboxUnavailable, + SandboxPreparation, +} + +impl ProcessStartErrorData { + pub fn attach_to(self, mut error: JSONRPCErrorError) -> JSONRPCErrorError { + error.data = Some(serde_json::json!(self)); + error + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecResponse { diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index e0f0a817d2..6817a9badb 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -10,6 +10,7 @@ use std::time::Duration; use crate::process_telemetry::ProcessTelemetry; use crate::process_telemetry::ProcessTelemetryEvent; use codex_exec_server_protocol::JSONRPCErrorError; +use codex_exec_server_protocol::ProcessStartErrorData; use codex_network_proxy::NetworkPolicyAuditEvent; use codex_network_proxy::NetworkPolicyAuditObserver; use codex_network_proxy::NetworkProtocol; @@ -456,7 +457,17 @@ impl LocalProcess { ) { process_map.remove(&process_id); } - return Err(internal_error(err.to_string())); + let io_error = err + .chain() + .find_map(|cause| cause.downcast_ref::()); + let kind = match io_error.map(std::io::Error::kind) { + Some(std::io::ErrorKind::NotFound) => ProcessStartErrorData::SpawnNotFound, + Some(std::io::ErrorKind::PermissionDenied) => { + ProcessStartErrorData::SpawnPermissionDenied + } + _ => ProcessStartErrorData::SpawnFailed, + }; + return Err(kind.attach_to(internal_error(err.to_string()))); } }; let metrics = self.inner.telemetry.process_started(&process_id); diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index da4ed7d5cf..088f537e9e 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::process_telemetry::ProcessTelemetry; use codex_exec_server_protocol::JSONRPCErrorError; +use codex_exec_server_protocol::ProcessStartErrorData; use codex_file_system::WindowsSandboxSelection; use codex_network_proxy::CHILD_ONLY_CA_ENV_KEYS; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; @@ -123,9 +124,11 @@ pub(crate) async fn prepare_exec_request_with_telemetry( )); } if !codex_sandboxing::windows_mxc_available() { - return Err(invalid_params( - "native MXC is unavailable on this executor".to_owned(), - )); + return Err( + ProcessStartErrorData::SandboxUnavailable.attach_to(invalid_params( + "native MXC is unavailable on this executor".to_owned(), + )), + ); } } let (env, managed_network, network_proxy_handle, network_proxy_restricting_sid) = @@ -150,8 +153,11 @@ pub(crate) async fn prepare_exec_request_with_telemetry( windows_sandbox: None, }); }; - let runtime_paths = runtime_paths - .ok_or_else(|| invalid_params("sandbox runtime paths are not configured".to_string()))?; + let runtime_paths = runtime_paths.ok_or_else(|| { + ProcessStartErrorData::SandboxUnavailable.attach_to(invalid_params( + "sandbox runtime paths are not configured".to_string(), + )) + })?; let windows_sandbox_proxy_settings_mode = sandbox_context .windows_sandbox_proxy_settings_mode .unwrap_or_default(); @@ -247,9 +253,9 @@ pub(crate) async fn prepare_exec_request_with_telemetry( }) .map_err(|err| match err { codex_sandboxing::SandboxTransformError::SandboxUnavailable => { - invalid_params(err.to_string()) + ProcessStartErrorData::SandboxUnavailable.attach_to(invalid_params(err.to_string())) } - err => invalid_params(format!("failed to prepare process sandbox: {err}")), + err => sandbox_preparation_error(err), })? .into_command_request(); request.sandbox_override = sandbox_context.sandbox_override; @@ -271,7 +277,7 @@ pub(crate) async fn prepare_exec_request_with_telemetry( windows_sandbox_level, &request.env, ) - .map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?; + .map_err(sandbox_preparation_error)?; Some(PreparedWindowsSandboxRequest { permission_profile: permissions, workspace_roots: native_workspace_roots, @@ -433,6 +439,12 @@ async fn prepare_managed_network( )) } +fn sandbox_preparation_error(err: impl std::fmt::Display) -> JSONRPCErrorError { + ProcessStartErrorData::SandboxPreparation.attach_to(invalid_params(format!( + "failed to prepare process sandbox: {err}" + ))) +} + fn native_path(path: &PathUri, label: &str) -> Result { path.to_abs_path().map_err(|err| { invalid_params(format!( diff --git a/codex-rs/exec-server/src/rpc_timing.rs b/codex-rs/exec-server/src/rpc_timing.rs index 6375d0b815..8fe33ae84b 100644 --- a/codex-rs/exec-server/src/rpc_timing.rs +++ b/codex-rs/exec-server/src/rpc_timing.rs @@ -2,8 +2,11 @@ use std::time::Instant; +use codex_exec_server_protocol::ProcessStartErrorData; +use serde::Deserialize; use serde_json::Value; +use crate::protocol::EXEC_METHOD; use crate::rpc::RpcCallError; pub(crate) struct RpcCompletion { @@ -20,9 +23,17 @@ impl RpcCompletion { } pub(crate) fn record_receipt(&self, method: &str) { - let outcome = match &self.result { - Ok(_) => "success", - Err(RpcCallError::Server(_)) => "error", + let (outcome, start_error_code, start_error_kind) = match &self.result { + Ok(_) => ("success", None, None), + Err(RpcCallError::Server(error)) if method == EXEC_METHOD => ( + "error", + Some(error.code), + error + .data + .as_ref() + .and_then(|data| ProcessStartErrorData::deserialize(data).ok()), + ), + Err(RpcCallError::Server(_)) => ("error", None, None), Err( RpcCallError::Closed | RpcCallError::Json(_) @@ -37,6 +48,8 @@ impl RpcCompletion { event.name = "codex.exec_server.response_received", rpc.method = method, outcome, + process.start_error_code = start_error_code, + process.start_error_kind = start_error_kind.as_ref().map(tracing::field::debug), reader_to_caller_ns = i64::try_from(self.completed_at.elapsed().as_nanos()) .unwrap_or(i64::MAX), );