Add structured error kinds for exec-server process starts (#61358)

## What changed

Attach a tagged `ProcessStartErrorData` value to JSON-RPC errors for not-found, permission-denied, and other spawn failures, unavailable sandboxes, and sandbox preparation failures.

Record the error code and structured error kind for failed `process/start` responses in the `codex.exec_server.response_received` tracing event.

GitOrigin-RevId: 556d0068e469ea25e95108262826a77a66b7d83b
This commit is contained in:
andrewgu-oai
2026-11-13 18:53:14 +00:00
committed by copyberry
parent 3f2603f2a2
commit dd8cee9822
4 changed files with 66 additions and 12 deletions
+12 -1
View File
@@ -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::<std::io::Error>());
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);
+20 -8
View File
@@ -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<AbsolutePathBuf, JSONRPCErrorError> {
path.to_abs_path().map_err(|err| {
invalid_params(format!(
+16 -3
View File
@@ -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),
);